Compare commits

...
Author SHA1 Message Date
Horizon agentandClaude Opus 5.5 7311870047 Pages: inactive, half and footer buttons, text rows and space (spec #14167)
Generic row options a page drawn as its own screen needs (Play with Friends'
page, #14167 step 2a): Row::button(..).active(false)/.half()/.footer(),
Row::text(..).quiet()/.groups(), Row::Space. The options shell shows text as
notes, pairs half buttons and ignores space; Extras shows active buttons only.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-11-09 20:55:32 +01:00
Ludovic Marie 12578352dc Merge pull request #14203 from Evan-lhm/rt/ray-query-opt-in
🤖 Agent: EXPERIMENTAL_RAY_QUERY is requested only with HORIZON_RAY_TRACING=1 in a horizon-extras build; without it the device request is main's.
2026-11-09 20:41:33 +01:00
Ludovic Marie 11ebc3f199 Merge pull request #13960 from horizon-rust/layers/packs-emissive
🤖 Agent: the emissive suffix is read once per pack load through PackFormats::emissive_suffix; atlas building only compares a stored string per sprite as before. layer plan M6 step 3.
2026-11-09 20:41:27 +01:00
Evan-lhm 3f1a50ccb2 Merge remote-tracking branch 'origin/main' into rt/ray-query-opt-in
# Conflicts:
#	crates/horizon-client/Cargo.toml
2026-11-09 19:25:47 +01:00
Evan-lhmandClaude Opus 5.5 75071c8b9b gfx.rs may not grow: ray_query is declared beside its one user
Its `mod` line moves from gfx.rs (over 3000 lines, docs/layers.md#file-size) into
surface_setup.rs, the device request that reads it.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-11-09 19:18:37 +01:00
Evan-lhmandClaude Opus 5.5 63a6a92bef Ray tracing on request is a Horizon extra (#13780)
As ludovic111 asked on #13780: asking the device for ray queries, and the log line
saying why it is on or off, happen only in a `horizon-extras` build
(horizon-client's feature now enables horizon-client-render's). A plain build
requests the device it requested before, whatever HORIZON_RAY_TRACING says
(`only_an_extras_build_asks_for_ray_tracing`, run with and without the feature).

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-11-09 19:07:35 +01:00
Evan-lhmandClaude Opus 5.5 cf93d0dd3a Renderer: ask for hardware ray queries only with HORIZON_RAY_TRACING=1 (#13780)
Step 1 of #13780 (carrying on the #1180 spike): the device requests wgpu's
EXPERIMENTAL_RAY_QUERY when the variable is set and the adapter has it, and
logs why it stays off otherwise. Nothing draws with it yet; without the
variable the device opens exactly as before. The unsafe opt-in lives in
horizon-hdr, which already holds the GPU calls the game crates can't make.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-11-09 19:03:49 +01:00
Horizon agentandClaude Opus 5.5 e39ec11e59 Merge branch 'layers/packs-colors' into layers/packs-emissive
Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-11-09 18:56:37 +01:00
Horizon agent 9fd16efb45 Merge branch 'layers/packs-colors' into layers/packs-emissive 2026-11-09 17:34:46 +01:00
Ludovic Marie 93ecc8d0c1 Merge branch 'layers/packs-colors' into layers/packs-emissive 2026-11-09 16:46:39 +01:00
Horizon agentandClaude Opus 5.5 2e45a0750d rustfmt
Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-11-09 16:27:26 +01:00
Horizon agent 8ad2c758ad Merge branch 'layers/packs-colors' into layers/packs-emissive 2026-11-09 16:27:26 +01:00
Horizon agentandClaude Opus 5.5 cbba6ead3a Emissive overlays' suffix through the pack-format hook
layer plan M6 (pack formats), step 3: the terrain's emissive overlays take
the pack's suffix from pack_hooks (OptiFine's emissive.properties, _e by
default); with no pack formats installed a pack has none. atlas_sources
checks a texture path stays in the pack root itself.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-11-09 15:39:02 +01:00
14 changed files with 342 additions and 39 deletions

No files matched your search

+4
View File
@@ -76,6 +76,10 @@ wgpu = { version = "30.0.1", default-features = false, features = ["gles"] }
audio = ["dep:cpal"]
# Helpers horizon-client's tests build their fixtures with.
test-support = []
# horizon-client's `horizon-extras`: asking the device for hardware ray tracing on request
# (`gfx/ray_query.rs`, `HORIZON_RAY_TRACING=1`). Without it the device is requested as without
# the variable.
horizon-extras = []
[lints]
workspace = true
+2 -2
View File
@@ -382,8 +382,8 @@ impl RenderAssets {
};
// After the connected tiles: a tile identical to a block shares its layer, and the
// overlays skip shared layers.
if draws {
emissive = crate::native_emissive::load_optifine(root, &mut sprites);
if draws && let Some(suffix) = crate::pack_hooks::formats().emissive_suffix(root) {
emissive = crate::native_emissive::load_overlays(root, &suffix, &mut sprites);
}
sprites.save_disk_cache();
if draws {
@@ -233,11 +233,11 @@ pub fn texture_path(root: &Path, texture: &str, directory: &str) -> PathBuf {
}
let (namespace, path) = texture.split_once(':').unwrap_or(("minecraft", texture));
let rel = format!("assets/{namespace}/{directory}/{path}.png");
root.join(if horizon_optifine::paths::valid(&rel) {
&rel
} else {
NOWHERE
})
// No `.`, `..` or empty segment, and no `\` (a separator on Windows, where `..\..` climbs).
let stays = rel
.split('/')
.all(|segment| !matches!(segment, "" | "." | "..") && !segment.contains('\\'));
root.join(if stays { &rel } else { NOWHERE })
}
/// `SpriteSourceList.list`: every sprite id of the atlas and its origin.
@@ -112,7 +112,11 @@ fn add_sections(groups: &mut Vec<Group<ExtraRow>>) {
};
Row::new(id, lang::get(label), value)
}
added::Row::Button { label } => Row::new(id, lang::get(label), Value::Press),
added::Row::Button {
label,
active: true,
..
} => Row::new(id, lang::get(label), Value::Press),
added::Row::Key { label, key } => {
let key = crate::keybinds::display_name(&key).plain();
Row::new(id, lang::get(label), Value::Read(key))
@@ -490,9 +494,7 @@ mod tests {
added::Place::HorizonSettings
}
fn rows(&self, rows: &mut Vec<added::Row>) {
rows.push(added::Row::Button {
label: "test.elsewhere.button",
});
rows.push(added::Row::button("test.elsewhere.button"));
}
fn act(&self, _: usize, _: Act) {}
}
@@ -0,0 +1,79 @@
//! Hardware ray queries, only on request (#13780, after the #1180 spike): `HORIZON_RAY_TRACING=1`
//! at startup asks the device for wgpu's `EXPERIMENTAL_RAY_QUERY` when the GPU has it. Nothing
//! draws with it yet. Without the variable, and in a build without `horizon-extras` (a Horizon
//! extra, docs/layers.md), the device is opened exactly as before.
/// The ray query feature.
const RAY_QUERY: wgpu::Features = wgpu::Features::EXPERIMENTAL_RAY_QUERY;
/// Whether the player asked for ray tracing (the device needs the feature from its creation).
fn requested() -> bool {
asked(std::env::var("HORIZON_RAY_TRACING").ok().as_deref())
}
/// Whether `HORIZON_RAY_TRACING` set to `var` asks for ray tracing: only in a `horizon-extras`
/// build.
fn asked(var: Option<&str>) -> bool {
cfg!(feature = "horizon-extras") && var == Some("1")
}
/// The features to request on `adapter`: ray queries, when asked for and offered.
pub(super) fn features(adapter: &wgpu::Adapter) -> wgpu::Features {
let features = pick(requested(), adapter.features());
if requested() {
let info = adapter.get_info();
if features.is_empty() {
log::warn!(
"Horizon renderer: ray tracing asked for, but {} on {:?} has no hardware ray \
queries; it stays off",
info.name,
info.backend
);
} else {
log::info!(
"Horizon renderer: ray queries on ({} on {:?})",
info.name,
info.backend
);
}
}
features
}
fn pick(requested: bool, offered: wgpu::Features) -> wgpu::Features {
if requested {
offered & RAY_QUERY
} else {
wgpu::Features::empty()
}
}
/// The device's opt-in to `features`' experimental ones.
pub(super) fn experimental(features: wgpu::Features) -> wgpu::ExperimentalFeatures {
if features.contains(RAY_QUERY) {
horizon_hdr::experimental_features()
} else {
wgpu::ExperimentalFeatures::disabled()
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn only_an_extras_build_asks_for_ray_tracing() {
assert_eq!(asked(Some("1")), cfg!(feature = "horizon-extras"));
assert!(!asked(None));
assert!(!asked(Some("0")));
}
#[test]
fn ray_queries_only_when_asked_and_offered() {
let all = wgpu::Features::all();
assert_eq!(pick(true, all), RAY_QUERY);
assert!(pick(false, all).is_empty());
assert!(pick(true, all - RAY_QUERY).is_empty());
assert!(!experimental(wgpu::Features::empty()).is_enabled());
}
}
@@ -5,6 +5,9 @@ use super::{
Renderer, check_layers, cull, describe_adapter, is_software, oit, startup_step, surface_format,
timing,
};
// Ray tracing on request, which the device needs from its creation (gfx.rs may not grow).
#[path = "ray_query.rs"]
mod ray_query;
use std::sync::Arc;
use winit::window::Window;
@@ -134,6 +137,7 @@ pub(super) async fn open_adapter(
let features = timing::GpuTimer::features(adapter)
| cull::Cull::features(adapter)
| oit::features(adapter)
| ray_query::features(adapter)
| (adapter.features()
& (wgpu::Features::INDIRECT_FIRST_INSTANCE
| wgpu::Features::MULTI_DRAW_INDIRECT_COUNT
@@ -166,6 +170,7 @@ pub(super) async fn open_adapter(
// with the larger blocks a few small buffers kept whole blocks the arena's pages had
// left, and over half the video memory reserved was unused (1,344 MiB for 500).
memory_hints: wgpu::MemoryHints::MemoryUsage,
experimental_features: ray_query::experimental(features),
..Default::default()
};
#[cfg(not(target_os = "macos"))]
@@ -1,6 +1,7 @@
//! Optional, texture-matched overlays drawn full bright over their base texture in the same
//! terrain fragment: native mods' (`<ns>:emissive/...`) and OptiFine's emissive block textures
//! (`block/stone_e.png` beside `block/stone.png`). No extra draw calls or world light changes.
//! terrain fragment: native mods' (`<ns>:emissive/...`) and a pack's emissive block textures
//! (OptiFine's `block/stone_e.png` beside `block/stone.png`, the suffix from the pack formats,
//! [`crate::pack_hooks`]). No extra draw calls or world light changes.
use crate::sprites::Sprites;
use std::{collections::BTreeMap, path::Path};
@@ -35,14 +36,12 @@ fn tile_file(name: &str) -> Option<&str> {
name.strip_suffix(".png")
}
/// OptiFine's emissive overlays of the block sprites already loaded (what the terrain draws;
/// items draw from the HUD atlas): for each
/// `<ns>:block/<name>`, or connected-texture tile `assets/<ns>/.../<name>.png`, whose
/// `<name><suffix>.png` sits beside it, the overlay is loaded too and paired with it, blocks
/// first, then tiles, each by name, at most [`MAX`] - [`NATIVE`] (native mods keep their share of
/// the table). A pack without such files adds nothing.
pub fn load_optifine(root: &Path, sprites: &mut Sprites) -> Vec<(String, String)> {
let suffix = horizon_optifine::emissive::suffix(root);
/// A pack's emissive overlays of the block sprites already loaded (what the terrain draws;
/// items draw from the HUD atlas): for each `<ns>:block/<name>`, or connected-texture tile
/// `assets/<ns>/.../<name>.png`, whose `<name><suffix>.png` sits beside it, the overlay is loaded
/// too and paired with it, blocks first, then tiles, each by name, at most [`MAX`] - [`NATIVE`]
/// (native mods keep their share of the table). A pack without such files adds nothing.
pub fn load_overlays(root: &Path, suffix: &str, sprites: &mut Sprites) -> Vec<(String, String)> {
// Blocks with identical pixels share a layer (and the shader pairs by layer): a shared
// base would glow on every block that shares it, so it gets no overlay.
let mut sharing: BTreeMap<u32, usize> = BTreeMap::new();
@@ -56,8 +55,8 @@ pub fn load_optifine(root: &Path, sprites: &mut Sprites) -> Vec<(String, String)
sharing[layer] == 1
&& (name
.split_once(':')
.is_some_and(|(_, p)| p.starts_with("block/") && !p.ends_with(&suffix))
|| tile_file(name).is_some_and(|stem| !stem.ends_with(&suffix)))
.is_some_and(|(_, p)| p.starts_with("block/") && !p.ends_with(suffix))
|| tile_file(name).is_some_and(|stem| !stem.ends_with(suffix)))
})
.map(|(name, _)| name.clone())
.collect();
@@ -110,7 +109,7 @@ pub fn load_optifine(root: &Path, sprites: &mut Sprites) -> Vec<(String, String)
pub const CUTOUT: u32 = 1 << 31;
/// The frame's overlay table: native mods' pairs (at most [`NATIVE`] requested), then the packs'
/// OptiFine pairs flagged [`CUTOUT`] (at most [`MAX`] - [`NATIVE`], as [`load_optifine`] loads),
/// OptiFine pairs flagged [`CUTOUT`] (at most [`MAX`] - [`NATIVE`], as [`load_overlays`] loads),
/// one per base layer with native mods' first, sorted by base layer for the shader's early exit.
pub fn table<'a>(
layers: &BTreeMap<String, u32>,
@@ -204,7 +203,7 @@ mod tests {
// Items draw from the HUD atlas, not the terrain: no pair.
sprites.insert(&root, "minecraft:item/stick").unwrap();
// glowstone isn't loaded: its overlay pairs with nothing.
let pairs = load_optifine(&root, &mut sprites);
let pairs = load_overlays(&root, "_e", &mut sprites);
assert_eq!(
pairs,
[(
@@ -215,9 +214,6 @@ mod tests {
assert!(sprites.layers.contains_key("minecraft:block/stone_e"));
assert!(!sprites.layers.contains_key("minecraft:block/granite_e"));
// The pack's own suffix.
let props = root.join("assets/minecraft/optifine/emissive.properties");
std::fs::create_dir_all(props.parent().unwrap()).unwrap();
std::fs::write(&props, "suffix.emissive=_glow").unwrap();
// Connected-texture tiles pair with the overlay beside them.
let tiles = root.join("assets/minecraft/optifine/ctm/lamp");
png(&tiles.join("0.png"), 230);
@@ -234,7 +230,7 @@ mod tests {
.insert_file(&root, &format!("assets/minecraft/optifine/{tile}.png"))
.unwrap();
}
let pairs = load_optifine(&root, &mut sprites);
let pairs = load_overlays(&root, "_glow", &mut sprites);
assert_eq!(
pairs,
// Blocks first, then tiles.
@@ -162,10 +162,38 @@ impl OptionsUi {
};
let mut rows = Vec::new();
registered.rows(&mut rows);
rows.into_iter()
.enumerate()
.map(|(r, row)| self.added_row(page, r, row))
.collect()
let mut out = Vec::new();
// The row a half-width button started, waiting for the next to sit beside it.
let mut half_open = false;
for (r, row) in rows.into_iter().enumerate() {
match row {
// Wrapped as the engine's own notes; space is for pages drawn as a screen.
added::Row::Text { text, .. } => {
out.extend(wrap(&text, 64).into_iter().map(Row::Note))
}
added::Row::Space(_) => continue,
added::Row::Button {
label,
active,
half: true,
..
} => {
let mut half = button(0.0, 0.0, 150.0, tr(label), Act::PageRow(page, r));
if !active {
half = inactive(half);
}
match out.last_mut() {
Some(Row::Buttons(_, second @ None)) if half_open => *second = Some(half),
_ => out.push(Row::Buttons(half, None)),
}
half_open = !half_open;
continue;
}
row => out.push(self.added_row(page, r, row)),
}
half_open = false;
}
out
}
fn added_row(&self, page: usize, r: usize, row: added::Row) -> Row {
@@ -177,7 +205,10 @@ impl OptionsUi {
added::Row::Toggle { label, on } => wide(generic(label, on_off(on))),
added::Row::Cycle { label, value } => wide(generic(label, label_text(value))),
added::Row::Slider { .. } => Row::Big(Opt::Page(page, r)),
added::Row::Button { label } => wide(tr(label)),
added::Row::Button { label, active, .. } => {
let wide = button(0.0, 0.0, 310.0, tr(label), act);
Row::Buttons(if active { wide } else { inactive(wide) }, None)
}
added::Row::Key { label, key } => {
let mut message = keybinds::display_name(&key);
if self.selected_key == Some(PAGE_KEY)
@@ -461,4 +492,84 @@ mod tests {
Some(tr("test.sample.switch.tooltip").plain())
);
}
/// Text, a grey note, space, two half buttons (the second inactive) and a footer button.
struct Shaped(Mutex<Vec<usize>>);
impl added::Page for Shaped {
fn id(&self) -> &'static str {
"test.shaped"
}
fn title(&self) -> &'static str {
"test.shaped.title"
}
fn place(&self) -> added::Place {
added::Place::HorizonSettings
}
fn rows(&self, rows: &mut Vec<added::Row>) {
rows.push(added::Row::text("ABCDE-FGHIJ").groups());
rows.push(added::Row::text("grey").quiet());
rows.push(added::Row::Space(6));
rows.push(added::Row::button("test.shaped.copy").half());
rows.push(added::Row::button("test.shaped.stop").half().active(false));
rows.push(added::Row::button("test.shaped.help").footer());
}
fn act(&self, row: usize, _: added::Act) {
self.0.lock().unwrap().push(row);
}
}
static SHAPED: Shaped = Shaped(Mutex::new(Vec::new()));
static SHAPED_PAGES: [&dyn added::Page; 1] = [&SHAPED];
#[test]
fn half_buttons_share_a_row_and_an_inactive_one_is_never_pressed() {
let mut s = Settings::default();
TEST_PAGES.set(Some(&SHAPED_PAGES));
let ui = OptionsUi::default();
let rows = ui.added_rows(0);
// The two texts as notes (space shows nothing here), the halves on one row, the
// footer button below them (the settings shell has no footer of its own).
assert!(matches!(&rows[0], Row::Note(t) if t == "ABCDE-FGHIJ"));
assert!(matches!(&rows[1], Row::Note(t) if t == "grey"));
let Row::Buttons(copy, Some(stop)) = &rows[2] else {
panic!("the halves sit side by side: {:?}", rows.len());
};
assert_eq!((copy.w, stop.w), (150.0, 150.0));
assert!(matches!(&rows[3], Row::Buttons(help, None) if help.w == 310.0));
assert_eq!(rows.len(), 4);
let mut ui = ui;
ui.show(Page::Added(0));
press(&mut ui, &mut s, Act::PageRow(0, 3), false);
press(&mut ui, &mut s, Act::PageRow(0, 5), false);
let (widgets, ..) = ui.layout(&s, 480.0, 240.0);
let stop = widgets
.iter()
.find(|w| {
matches!(
&w.kind,
WKind::Button {
act: Act::PageRow(0, 4),
..
}
)
})
.expect("Stop is drawn");
let atlas = super::super::tests::atlas();
ui.click(
&mut s,
&atlas,
stop.x + 1.0,
stop.y + 1.0,
(480.0, 240.0),
[480, 240],
false,
false,
);
assert_eq!(
*SHAPED.0.lock().unwrap(),
[3, 5],
"the inactive half heard nothing"
);
}
}
@@ -20,6 +20,10 @@ impl PackFormats for Formats {
Some(colors::read(root))
}
fn emissive_suffix(&self, root: &Path) -> Option<String> {
Some(horizon_optifine::emissive::suffix(root))
}
fn custom_sky(&self, root: &Path) -> Option<Arc<dyn crate::custom_sky::SkyLayers>> {
let sky = custom_sky::CustomSky::load(root);
(!sky.is_empty()).then(|| Arc::new(sky) as _)
@@ -15,6 +15,12 @@ pub trait PackFormats: Send + Sync {
None
}
/// The name ending of the overlay's emissive textures (`stone_e.png` beside `stone.png`),
/// if its format has them.
fn emissive_suffix(&self, _root: &Path) -> Option<String> {
None
}
/// The sky layers the overlay draws over the vanilla sky, if it has any.
fn custom_sky(&self, _root: &Path) -> Option<Arc<dyn crate::custom_sky::SkyLayers>> {
None
+3 -2
View File
@@ -119,10 +119,11 @@ audio = ["horizon-client-render/audio", "horizon-client-lib/audio"]
controllers = ["horizon-client-lib/controllers", "dep:gilrs", "dep:horizon-dualsense", "horizon-client-input/controllers"]
# Horizon's own extras over the vanilla game, for a fork that wants them out (docs/forking.md):
# cosmetics and emotes, the "Horizon player" presence, the official server added to
# the server list, the Horizon Settings and Mods pages, Discord and Play with Friends. Players always get them; there is no switch. `grep -rn horizon-extras crates/` lists
# the server list, the Horizon Settings and Mods pages, Discord and Play with Friends, and ray
# tracing on request (`HORIZON_RAY_TRACING=1`). Players always get them; there is no switch. `grep -rn horizon-extras crates/` lists
# every place it gates. Without it (`--no-default-features --features audio,controllers`) the
# client is best effort: CI builds only the default.
horizon-extras = ["horizon-client-lib/horizon-extras", "discord", "friends"]
horizon-extras = ["horizon-client-lib/horizon-extras", "horizon-client-render/horizon-extras", "discord", "friends"]
# Online extras a server or a port can leave out; the server build has none of them. "Playing Horizon"
# on Discord (game/discord.rs).
discord = ["horizon-client-lib/discord"]
+9
View File
@@ -47,6 +47,15 @@ pub fn open_vulkan_device(
Some(unsafe { adapter.create_device_from_hal(open, desc) })
}
/// wgpu's opt-in to its `EXPERIMENTAL_*` features, which the crates that forbid unsafe code
/// can't give: hardware ray queries, asked for only when the player turns ray tracing on
/// (`gfx/ray_query.rs` in horizon-client-render).
pub fn experimental_features() -> wgpu::ExperimentalFeatures {
// Safety: wgpu warns that its experimental features may have bugs that reach undefined
// behaviour through safe calls. Horizon accepts that only on request, for ray queries.
unsafe { wgpu::ExperimentalFeatures::enabled() }
}
/// Tells the compositor, through `VK_EXT_hdr_metadata`, that the swapchain's
/// content is BT.709, peaks at `peak` nits and is no darker than `black`. Needs a
/// device from `open_vulkan_device` and a configured surface; does nothing otherwise.
+82 -2
View File
@@ -61,13 +61,93 @@ pub enum Row {
value: f32,
text: Label,
},
/// A button that does something.
Button { label: &'static str },
/// A button that does something ([`Row::button`]). An inactive one is drawn greyed and
/// never pressed; two half-width ones in a row sit side by side; a footer one sits at the
/// bottom beside Done on a page drawn as its own screen, and with the others elsewhere.
/// Horizon Settings' Extras shows only active ones.
#[non_exhaustive]
Button {
label: &'static str,
active: bool,
half: bool,
footer: bool,
},
/// Text shown as it is, wrapped to the page's width ([`Row::text`]): white, or grey when
/// `quiet` (secondary); `groups` breaks it only between its `-`-separated groups (a code).
#[non_exhaustive]
Text {
text: String,
quiet: bool,
groups: bool,
},
/// Empty space, in GUI pixels, on a page drawn as its own screen; nothing elsewhere.
Space(u8),
/// A key binding, as Key Binds shows one: `key` is its key name (`key.keyboard.z`,
/// `key.mouse.left`). A press waits for the next key or mouse button ([`Act::Bind`]).
Key { label: &'static str, key: String },
}
impl Row {
/// An active, full-width button in the page's body.
pub fn button(label: &'static str) -> Self {
Self::Button {
label,
active: true,
half: false,
footer: false,
}
}
/// White text in the page's body, wrapped at spaces.
pub fn text(text: impl Into<String>) -> Self {
Self::Text {
text: text.into(),
quiet: false,
groups: false,
}
}
/// A button greyed out (`false`): drawn, never pressed.
pub fn active(mut self, on: bool) -> Self {
if let Self::Button { active, .. } = &mut self {
*active = on;
}
self
}
/// A button half as wide: two in a row sit side by side.
pub fn half(mut self) -> Self {
if let Self::Button { half, .. } = &mut self {
*half = true;
}
self
}
/// A button in the footer, beside Done, on a page drawn as its own screen.
pub fn footer(mut self) -> Self {
if let Self::Button { footer, .. } = &mut self {
*footer = true;
}
self
}
/// Text in grey: secondary.
pub fn quiet(mut self) -> Self {
if let Self::Text { quiet, .. } = &mut self {
*quiet = true;
}
self
}
/// Text broken only between its `-`-separated groups, never inside one (a code).
pub fn groups(mut self) -> Self {
if let Self::Text { groups, .. } = &mut self {
*groups = true;
}
self
}
}
/// A value shown on a row: a translation key, or text shown as it is (a number, a name).
#[derive(Clone, Debug, PartialEq)]
pub enum Label {
+7 -1
View File
@@ -379,8 +379,10 @@ pub enum Row {
Toggle { label: &'static str, on: bool },
Cycle { label: &'static str, value: Label },
Slider { label: &'static str, value: f32, text: Label }, // value in 0..=1
Button { label: &'static str },
Button { label: &'static str, active: bool, half: bool, footer: bool }, // Row::button(..).half()...
Key { label: &'static str, key: String }, // a binding: `key.keyboard.z`
Text { text: String, quiet: bool, groups: bool }, // Row::text(..): white or grey, wrapped
Space(u8), // GUI pixels, on a page drawn as a screen
}
pub enum Label { Key(&'static str), Value(String) }
pub enum Act { Press, PressBack, Slide(f32), Bind(String) }
@@ -390,6 +392,10 @@ pub enum Act { Press, PressBack, Slide(f32), Bind(String) }
button on the title screen or the pause menu that opens the page. These buttons exist only when something
registered, so the vanilla menus keep vanilla's look. The page is drawn with the same widgets, tooltips,
narration, controller focus and `horizon-ctl` ids as the engine's pages.
- `Button` and `Text` are built with `Row::button` and `Row::text` and their options: `.active(false)` greys a
button, `.half()` puts two side by side, `.footer()` sets one beside Done on a page drawn as its own screen,
`.quiet()` greys text, `.groups()` breaks it only between `-`-separated groups (an invite code). In the options
shell, text is notes and space is nothing.
- Rows are rebuilt when the page is drawn or acted on, like the engine's own pages: no cost while no page is
open.
- The engine keeps native mods' pages (`Page::NativeMod`) as they are. Both kinds sit in one module that