mirror of
https://github.com/horizon-rust/horizon.git
synced 2026-11-07 11:06:11 -10:00
Create World: the preparing screen and the experimental-features warning
Vanilla reads the world generation data behind a loading screen before the Create New World screen opens, again after its Data Packs or Experiments screen applied a selection, and warns before applying a selection that asks for experimental features. Horizon showed none of that. - `createWorld.preparing` (`GenericMessageScreen`: the menu background and the centred message on a black box with a 1-pixel border and 12 pixels of padding, no progress): Create New World reads `vanillagen::shared_data` on a worker while the title shows it, and opens the create screen when the read is done; the Data Packs screen's Done and the Experiments screen's Done re-run the read the same way (`applyNewPackConfig`). - `ConfirmExperimentalFeaturesScreen`: when the Data Packs screen's Done picks a feature pack, the warning comes up first — the title, the red message wrapped to 310 and centred, a 100-wide Details button, Proceed and Back in the footer, the whole block centred, Escape declining. Its `DetailsScreen` lists the picked packs bold with the flags they ask for (`selectWorld.experimental.details.entry`). Proceed applies the selection, Back reopens the Data Packs screen on the selection it was opened with (`tryApplyNewDataPacks`'s `onAbort`). Stacked on #4619, whose Experiments screen and feature-pack plumbing the confirm screen builds on. Co-Authored-By: GLM 5.3 (ZCode) <[email protected]>
This commit is contained in:
1 parent
228b7d3491
commit
072cc61277
6 files changed
+440
-9
No files matched your search
@@ -2058,6 +2058,9 @@ struct Game<'a> {
|
||||
lectern_pulses: Vec<((i32, i32, i32), i32)>,
|
||||
/// LevelLoadingScreen.smoothedProgress while the player is not placed yet.
|
||||
loading_progress: f32,
|
||||
/// `CreateWorldScreen.openCreateWorldScreen`'s data read, on a worker,
|
||||
/// while the title says `createWorld.preparing`.
|
||||
creation_job: Option<std::thread::JoinHandle<()>>,
|
||||
/// When `loading_progress` last eased (the server thread's load, 50 ms a step).
|
||||
loading_progress_at: Option<std::time::Instant>,
|
||||
/// The world spawn search's origin chunk and fallback height, once known.
|
||||
@@ -15931,6 +15934,7 @@ impl<'a> Game<'a> {
|
||||
book_last_click: None,
|
||||
lectern_pulses: Vec::new(),
|
||||
loading_progress: 0.0,
|
||||
creation_job: None,
|
||||
loading_progress_at: None,
|
||||
spawn_origin: None,
|
||||
spawn_spiral: Default::default(),
|
||||
@@ -16327,6 +16331,7 @@ impl Game<'_> {
|
||||
fn resume_world_entry(&mut self) -> Result<()> {
|
||||
self.poll_integrated_start()?;
|
||||
self.poll_world_rest()?;
|
||||
self.poll_world_creation();
|
||||
if let Some(loader) = &self.world_loader {
|
||||
if !loader.ready() {
|
||||
return Ok(());
|
||||
@@ -16382,6 +16387,44 @@ impl Game<'_> {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// `CreateWorldScreen.openCreateWorldScreen`: read the world generation
|
||||
/// data the create screen describes, on a worker, then open the screen
|
||||
/// (`title.preparing`).
|
||||
fn poll_world_creation(&mut self) {
|
||||
let Some(title) = self.title.as_ref() else {
|
||||
return;
|
||||
};
|
||||
if !title.preparing() {
|
||||
return;
|
||||
}
|
||||
if self.creation_job.is_none() {
|
||||
let dir = self.options.resources.join("worldgen");
|
||||
let registry = self.pack.assets.registry;
|
||||
self.creation_job = std::thread::Builder::new()
|
||||
.name("world-creation".into())
|
||||
.spawn(move || {
|
||||
if let Err(e) = horizon_client::vanillagen::shared_data(&dir, registry) {
|
||||
log::warn!("Horizon: preparing world creation failed: {e}");
|
||||
}
|
||||
})
|
||||
.ok();
|
||||
return;
|
||||
}
|
||||
if !self
|
||||
.creation_job
|
||||
.as_ref()
|
||||
.is_none_or(std::thread::JoinHandle::is_finished)
|
||||
{
|
||||
return;
|
||||
}
|
||||
if let Some(job) = self.creation_job.take() {
|
||||
let _ = job.join();
|
||||
}
|
||||
if let Some(title) = self.title.as_mut() {
|
||||
title.finish_preparing();
|
||||
}
|
||||
}
|
||||
|
||||
fn poll_world_rest(&mut self) -> Result<()> {
|
||||
if let Some(loader) = &self.rest_loader
|
||||
&& loader.ready()?
|
||||
|
||||
@@ -302,8 +302,9 @@ impl Game<'_> {
|
||||
super::packs::open_folder(&dir);
|
||||
}
|
||||
Event::DataPacks(ids) => {
|
||||
let resources = self.vanilla_root();
|
||||
if let Some(title) = &mut self.title {
|
||||
title.set_data_packs(ids);
|
||||
title.offer_data_packs(&resources, ids);
|
||||
}
|
||||
}
|
||||
Event::OpenDataPackFolder(dir) => {
|
||||
|
||||
@@ -420,8 +420,9 @@ impl Loading {
|
||||
packs::open_folder(&dir);
|
||||
}
|
||||
Event::DataPacks(ids) => {
|
||||
let resources = self.vanilla_root.clone();
|
||||
if let Some(title) = &mut self.menu.title {
|
||||
title.set_data_packs(ids);
|
||||
title.offer_data_packs(&resources, ids);
|
||||
}
|
||||
}
|
||||
Event::OpenDataPackFolder(dir) => {
|
||||
|
||||
@@ -15,6 +15,7 @@ use std::{
|
||||
sync::mpsc,
|
||||
};
|
||||
|
||||
mod confirm_experiments;
|
||||
mod create_world;
|
||||
mod experiments;
|
||||
mod flat;
|
||||
@@ -435,6 +436,13 @@ pub struct Title {
|
||||
pub onboarding_finished: bool,
|
||||
/// CreateWorldScreen's Experiments screen, over Create New World.
|
||||
experiments: RefCell<Option<experiments::ExperimentsScreen>>,
|
||||
/// ConfirmExperimentalFeaturesScreen, over Create New World.
|
||||
confirm_experiments: RefCell<Option<confirm_experiments::ConfirmExperimentsScreen>>,
|
||||
/// The world creation data is being read: the screen says
|
||||
/// `createWorld.preparing` until it is done.
|
||||
preparing: bool,
|
||||
/// The confirm screen's Back: the Data Packs screen reopens.
|
||||
reopen_data_packs: bool,
|
||||
}
|
||||
pub(crate) struct Button {
|
||||
x: f32,
|
||||
@@ -669,6 +677,9 @@ impl Title {
|
||||
rules: RefCell::new(None),
|
||||
onboarding_finished: false,
|
||||
experiments: RefCell::new(None),
|
||||
confirm_experiments: RefCell::new(None),
|
||||
preparing: false,
|
||||
reopen_data_packs: false,
|
||||
}
|
||||
}
|
||||
/// Vanilla bypasses the selector when there are no saves.
|
||||
@@ -805,6 +816,19 @@ impl Title {
|
||||
};
|
||||
}
|
||||
}
|
||||
/// `createWorld.preparing` is up: the world creation data is being read.
|
||||
pub fn preparing(&self) -> bool {
|
||||
self.preparing
|
||||
}
|
||||
/// The read finished: the flag goes, and the create screen opens — a
|
||||
/// fresh one entering from the world list, the one the player already
|
||||
/// filled in after the data packs changed (`openCreateWorldScreen`).
|
||||
pub fn finish_preparing(&mut self) {
|
||||
self.preparing = false;
|
||||
if !matches!(self.mode, Mode::Create { .. }) {
|
||||
self.mode = Self::new_world();
|
||||
}
|
||||
}
|
||||
/// The Data Packs screen's Done (`applyNewPackConfig`): its selection for
|
||||
/// the new world.
|
||||
/// `PackSelectionScreen.onFilesDrop`: a pack dropped on the window joins
|
||||
@@ -824,6 +848,41 @@ impl Title {
|
||||
log::warn!("Horizon: {} is not a data pack", path.display());
|
||||
}
|
||||
}
|
||||
/// The world's data pack selection, for the game to reoffer.
|
||||
pub fn data_packs(&self) -> Vec<String> {
|
||||
match &self.mode {
|
||||
Mode::Create { options, .. } => options.data_packs.clone(),
|
||||
_ => Vec::new(),
|
||||
}
|
||||
}
|
||||
/// The new world's pack folder, for the Data Packs screen to reopen.
|
||||
pub fn pack_dir(&self) -> Option<PathBuf> {
|
||||
match &self.mode {
|
||||
Mode::Create { options, .. } => options.pack_dir.clone(),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
/// The Data Packs screen's Done (`tryApplyNewDataPacks`): a selection
|
||||
/// with feature packs asks for experimental features, so the confirm
|
||||
/// screen comes up first; anything else applies at once and the data
|
||||
/// is read again.
|
||||
pub fn offer_data_packs(&mut self, resources: &std::path::Path, ids: Vec<String>) {
|
||||
let experimental = crate::datapack::feature_packs(resources)
|
||||
.into_iter()
|
||||
.any(|p| ids.contains(&p.id));
|
||||
if experimental {
|
||||
*self.confirm_experiments.get_mut() = Some(
|
||||
confirm_experiments::ConfirmExperimentsScreen::new(resources, &ids),
|
||||
);
|
||||
} else {
|
||||
self.set_data_packs(ids);
|
||||
self.preparing = true;
|
||||
}
|
||||
}
|
||||
/// Whether the confirm screen declined: the Data Packs screen reopens.
|
||||
pub fn take_reopen_data_packs(&mut self) -> bool {
|
||||
std::mem::take(&mut self.reopen_data_packs)
|
||||
}
|
||||
pub fn set_data_packs(&mut self, ids: Vec<String>) {
|
||||
if let Mode::Create { options, .. } = &mut self.mode {
|
||||
options.data_packs = ids;
|
||||
@@ -1879,6 +1938,27 @@ impl Title {
|
||||
if let Some(rules) = self.rules.borrow_mut().as_mut() {
|
||||
return rules.draw(batch, atlas, cursor);
|
||||
}
|
||||
if self.preparing {
|
||||
// `CreateWorldScreen.openCreateWorldScreen`'s queued
|
||||
// `GenericMessageScreen`: the menu background and the message
|
||||
// alone, centred, no progress.
|
||||
crate::ui::menu_background(batch, atlas, 0.0, 0.0, w, h, false);
|
||||
let text =
|
||||
crate::lang::get_or("createWorld.preparing", "Preparing for world creation...");
|
||||
let tw = atlas.font.width_text(&Text::literal(text.clone()));
|
||||
let (tx, ty) = (w / 2.0 - tw / 2.0, h / 2.0 - 4.5);
|
||||
// `FocusableTextWidget.builder(title, font, 12)`: 12 pixels of
|
||||
// padding around the text, on a black box with a 1-pixel border.
|
||||
let (bx, by) = (tx - 12.0, ty - 12.0);
|
||||
let (bw, bh) = (tw + 24.0, 9.0 + 24.0);
|
||||
batch.fill(atlas, bx - 1.0, by - 1.0, bw + 2.0, bh + 2.0, 0xFFD0D0D0);
|
||||
batch.fill(atlas, bx, by, bw, bh, 0xFF000000);
|
||||
batch.text(atlas, &text, tx, ty, u32::MAX, false);
|
||||
return;
|
||||
}
|
||||
if let Some(screen) = self.confirm_experiments.borrow().as_ref() {
|
||||
return screen.draw(batch, atlas, cursor);
|
||||
}
|
||||
if let Some(screen) = self.experiments.borrow().as_ref() {
|
||||
return screen.draw(batch, atlas, cursor);
|
||||
}
|
||||
@@ -2663,7 +2743,11 @@ impl Title {
|
||||
pub fn click(&mut self, x: f32, y: f32, size: (f32, f32)) -> (Action, bool) {
|
||||
self.remember_direct_address();
|
||||
// The game rules take the press in `press_rules`, which has the atlas.
|
||||
if self.rules.get_mut().is_some() || self.experiments.get_mut().is_some() {
|
||||
if self.rules.get_mut().is_some()
|
||||
|| self.experiments.get_mut().is_some()
|
||||
|| self.confirm_experiments.get_mut().is_some()
|
||||
|| self.preparing
|
||||
{
|
||||
return (Action::None, false);
|
||||
}
|
||||
let (w, h) = (size.0.ceil(), size.1.ceil());
|
||||
@@ -2837,7 +2921,10 @@ impl Title {
|
||||
}
|
||||
(Mode::Select { .. }, Some(11)) => {
|
||||
self.create_return_to_main = false;
|
||||
self.mode = Self::new_world();
|
||||
// `CreateWorldScreen.openCreateWorldScreen`: the world
|
||||
// generation data is read first, behind a loading screen;
|
||||
// the game opens the create screen after (`preparing`).
|
||||
self.preparing = true;
|
||||
Action::None
|
||||
}
|
||||
(Mode::Select { worlds, selected }, Some(id @ 13..=15)) => {
|
||||
@@ -3296,6 +3383,57 @@ impl Title {
|
||||
/// it lands on a button, for its sound.
|
||||
pub fn press_rules(&mut self, atlas: &HudAtlas, cursor: (f32, f32), size: (f32, f32)) -> bool {
|
||||
let size = (size.0.ceil(), size.1.ceil());
|
||||
if self.confirm_experiments.borrow().is_some() {
|
||||
let click = self
|
||||
.confirm_experiments
|
||||
.borrow()
|
||||
.as_ref()
|
||||
.and_then(|s| s.click(atlas, cursor, size));
|
||||
match click {
|
||||
Some(confirm_experiments::Click::Details) => {
|
||||
self.confirm_experiments
|
||||
.borrow_mut()
|
||||
.as_mut()
|
||||
.unwrap()
|
||||
.details = true;
|
||||
return true;
|
||||
}
|
||||
Some(confirm_experiments::Click::Proceed) => {
|
||||
let pending = self
|
||||
.confirm_experiments
|
||||
.borrow_mut()
|
||||
.take()
|
||||
.map(|s| s.pending);
|
||||
// `tryApplyNewDataPacks` accepted: the packs apply, and
|
||||
// their data is read again (`applyNewPackConfig`'s load
|
||||
// screen).
|
||||
if let Some(pending) = pending {
|
||||
self.set_data_packs(pending);
|
||||
self.preparing = true;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
Some(confirm_experiments::Click::Back) => {
|
||||
let screen = self.confirm_experiments.borrow();
|
||||
let in_details = screen.as_ref().is_some_and(|s| s.details);
|
||||
drop(screen);
|
||||
if in_details {
|
||||
self.confirm_experiments
|
||||
.borrow_mut()
|
||||
.as_mut()
|
||||
.unwrap()
|
||||
.details = false;
|
||||
} else {
|
||||
// `onAbort`: the confirm declines, the Data Packs
|
||||
// screen reopens (the game side holds the folder).
|
||||
self.confirm_experiments.borrow_mut().take();
|
||||
self.reopen_data_packs = true;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
None => return false,
|
||||
}
|
||||
}
|
||||
if self.experiments.borrow().is_some() {
|
||||
let click = self
|
||||
.experiments
|
||||
@@ -3323,8 +3461,13 @@ impl Title {
|
||||
}
|
||||
return true;
|
||||
}
|
||||
Some(experiments::Click::Outcome(_)) => {
|
||||
Some(experiments::Click::Outcome(outcome)) => {
|
||||
*self.experiments.get_mut() = None;
|
||||
// `applyNewPackConfig`'s load screen: the data is read
|
||||
// again for the packs the screen picked (Done only).
|
||||
if outcome == experiments::Outcome::Done {
|
||||
self.preparing = true;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
None => return false,
|
||||
@@ -3398,6 +3541,10 @@ impl Title {
|
||||
}
|
||||
}
|
||||
pub fn escape(&mut self) {
|
||||
if self.confirm_experiments.get_mut().take().is_some() {
|
||||
self.reopen_data_packs = true;
|
||||
return;
|
||||
}
|
||||
if self.experiments.get_mut().take().is_some() {
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -0,0 +1,237 @@
|
||||
//! CreateWorldScreen's `ConfirmExperimentalFeaturesScreen`: the warning that
|
||||
//! the picked data packs ask for experimental features, with the packs' own
|
||||
//! flags on a second screen (`DetailsScreen`).
|
||||
//!
|
||||
//! Vanilla centres a grid (columns 10 apart, rows 20 apart): the title, the
|
||||
//! message wrapped to 310, a 100-wide Details button, then Proceed and Back.
|
||||
|
||||
use crate::{
|
||||
font::Font,
|
||||
hud::{HudAtlas, HudBatch},
|
||||
text::{Style, Text, named_color},
|
||||
ui,
|
||||
};
|
||||
|
||||
/// `ConfirmExperimentalFeaturesScreen.COLUMN_SPACING`.
|
||||
const COLUMN_SPACING: f32 = 10.0;
|
||||
/// The footer buttons' width (`Button.defaults`).
|
||||
const BUTTON: f32 = 150.0;
|
||||
/// `DETAILS_BUTTON_WIDTH`.
|
||||
const DETAILS: f32 = 100.0;
|
||||
/// `MultiLineTextWidget.setMaxWidth(310)`.
|
||||
const WARNING: f32 = 310.0;
|
||||
/// One button row plus the grid's 20-pixel row spacing, twice.
|
||||
const ROW: f32 = 40.0;
|
||||
|
||||
/// A press on the screen.
|
||||
#[derive(Clone, Copy, Debug, PartialEq)]
|
||||
pub enum Click {
|
||||
/// The Details button: the packs' flags.
|
||||
Details,
|
||||
/// `gui.proceed`: apply the pending selection.
|
||||
Proceed,
|
||||
/// `gui.back` or Escape: leave the selection as it was.
|
||||
Back,
|
||||
}
|
||||
|
||||
/// One enabled feature pack, as `DetailsScreen`'s list shows it.
|
||||
pub struct Entry {
|
||||
/// The pack's name, bold.
|
||||
pub title: Text,
|
||||
/// `selectWorld.experimental.details.entry`'s argument: the flags.
|
||||
pub flags: String,
|
||||
}
|
||||
|
||||
/// The screen; `details` shows `DetailsScreen` instead.
|
||||
pub struct ConfirmExperimentsScreen {
|
||||
pub entries: Vec<Entry>,
|
||||
/// The selection to apply on Proceed.
|
||||
pub pending: Vec<String>,
|
||||
pub details: bool,
|
||||
in_world: bool,
|
||||
}
|
||||
impl ConfirmExperimentsScreen {
|
||||
/// `ConfirmExperimentalFeaturesScreen::new`: the picked selection's
|
||||
/// feature packs, each with the flags it asks for.
|
||||
pub fn new(resources: &std::path::Path, selected: &[String]) -> Self {
|
||||
let entries = crate::datapack::feature_packs(resources)
|
||||
.into_iter()
|
||||
.filter(|p| selected.contains(&p.id))
|
||||
.filter(|p| !p.features().is_empty())
|
||||
.map(|p| {
|
||||
let name = format!("dataPack.{}.name", p.id);
|
||||
let title = (if crate::lang::has(&name) {
|
||||
Text::translate(&name, &[])
|
||||
} else {
|
||||
Text::literal(p.id.clone())
|
||||
})
|
||||
.with_style(&Style {
|
||||
bold: Some(true),
|
||||
..Style::default()
|
||||
});
|
||||
let flags = p
|
||||
.features()
|
||||
.iter()
|
||||
.map(|f| f.trim_start_matches("minecraft:").to_owned())
|
||||
.collect::<Vec<_>>()
|
||||
.join(", ");
|
||||
Entry { title, flags }
|
||||
})
|
||||
.collect();
|
||||
Self {
|
||||
entries,
|
||||
pending: selected.to_vec(),
|
||||
details: false,
|
||||
in_world: false,
|
||||
}
|
||||
}
|
||||
/// The message's lines (`MultiLineTextWidget` at 310; the text's own
|
||||
/// line breaks kept).
|
||||
fn message_lines(font: &Font) -> Vec<Text> {
|
||||
crate::lang::get_or(
|
||||
"selectWorld.experimental.message",
|
||||
"Be careful!\nThis configuration requires features that are still under development. \
|
||||
Your world might crash, break, or not work with future updates.",
|
||||
)
|
||||
.split('\n')
|
||||
.flat_map(|line| {
|
||||
font.split_lines(&Text::literal(line.to_owned()), WARNING)
|
||||
.into_iter()
|
||||
.map(|(l, _)| l)
|
||||
})
|
||||
.collect()
|
||||
}
|
||||
/// The grid's top: the whole block centred vertically
|
||||
/// (`FrameLayout.alignInRectangle`).
|
||||
fn block_top(message: usize, h: f32) -> f32 {
|
||||
let rows = 9.0 + message as f32 * 9.0 + ROW + ROW;
|
||||
((h - rows) / 2.0).max(0.0)
|
||||
}
|
||||
/// The Details button's rect (centred, under the message).
|
||||
fn details_rect(message: usize, w: f32, h: f32) -> (f32, f32) {
|
||||
(
|
||||
((w - DETAILS) / 2.0).trunc(),
|
||||
Self::block_top(message, h) + 9.0 + ROW,
|
||||
)
|
||||
}
|
||||
/// A press: Details, Proceed (`gui.proceed`) or Back (`gui.back`).
|
||||
pub fn click(&self, atlas: &HudAtlas, (x, y): (f32, f32), size: (f32, f32)) -> Option<Click> {
|
||||
let (w, h) = size;
|
||||
if self.details {
|
||||
let [(bx, by)] = footer_single(w, h);
|
||||
return ui::hovered((x, y), bx, by, BUTTON, 20.0).then_some(Click::Back);
|
||||
}
|
||||
let message = Self::message_lines(&atlas.font).len();
|
||||
let (dx, dy) = Self::details_rect(message, w, h);
|
||||
if ui::hovered((x, y), dx, dy, DETAILS, 20.0) {
|
||||
return Some(Click::Details);
|
||||
}
|
||||
let [(px, py), (bx, by)] = footer_pair(w, h);
|
||||
if ui::hovered((x, y), px, py, BUTTON, 20.0) {
|
||||
return Some(Click::Proceed);
|
||||
}
|
||||
ui::hovered((x, y), bx, by, BUTTON, 20.0).then_some(Click::Back)
|
||||
}
|
||||
pub fn draw(&self, batch: &mut HudBatch, atlas: &HudAtlas, cursor: (f32, f32)) {
|
||||
let (w, h) = (batch.width, batch.height);
|
||||
ui::menu_background(batch, atlas, 0.0, 0.0, w, h, self.in_world);
|
||||
let font = &atlas.font;
|
||||
if self.details {
|
||||
// `DetailsScreen`: the header's title, the packs, Back in the
|
||||
// footer.
|
||||
let title = Text::translate("selectWorld.experimental.details.title", &[]);
|
||||
let tw = font.width_text(&title);
|
||||
batch.text_styled(
|
||||
atlas,
|
||||
&title,
|
||||
((w - tw) / 2.0).trunc(),
|
||||
15.0,
|
||||
u32::MAX,
|
||||
true,
|
||||
);
|
||||
let gray = named_color("gray").unwrap_or(0xAAAAAA);
|
||||
let mut y = 63.0 + 30.0;
|
||||
for entry in &self.entries {
|
||||
let x = (w / 2.0 - 155.0).trunc();
|
||||
batch.text_styled(atlas, &entry.title, x, y, u32::MAX, true);
|
||||
y += 12.0;
|
||||
let line = Text::translate("selectWorld.experimental.details.entry", &[])
|
||||
.append(Text::literal(entry.flags.clone()).with_color(gray));
|
||||
for (l, _) in font.split_lines(&line, WARNING) {
|
||||
batch.text_styled(atlas, &l, x, y, u32::MAX, true);
|
||||
y += 9.0;
|
||||
}
|
||||
y += 10.0;
|
||||
}
|
||||
let [(bx, by)] = footer_single(w, h);
|
||||
ui::button(
|
||||
batch,
|
||||
atlas,
|
||||
bx,
|
||||
by,
|
||||
BUTTON,
|
||||
20.0,
|
||||
&Text::translate("gui.back", &[]),
|
||||
true,
|
||||
ui::hovered(cursor, bx, by, BUTTON, 20.0),
|
||||
);
|
||||
return;
|
||||
}
|
||||
let message = Self::message_lines(font);
|
||||
let top = Self::block_top(message.len(), h);
|
||||
// The title, the message, Details, then the two buttons: the grid's
|
||||
// rows 20 apart, the whole block centred.
|
||||
let title = Text::translate("selectWorld.experimental.title", &[]);
|
||||
let tw = font.width_text(&title);
|
||||
batch.text_styled(atlas, &title, ((w - tw) / 2.0).trunc(), top, u32::MAX, true);
|
||||
let (dx, dy) = Self::details_rect(message.len(), w, h);
|
||||
let mut y = top + 9.0;
|
||||
for line in &message {
|
||||
batch.text_styled(
|
||||
atlas,
|
||||
line,
|
||||
((w - WARNING) / 2.0).trunc(),
|
||||
y,
|
||||
u32::MAX,
|
||||
true,
|
||||
);
|
||||
y += 9.0;
|
||||
}
|
||||
ui::button(
|
||||
batch,
|
||||
atlas,
|
||||
dx,
|
||||
dy,
|
||||
DETAILS,
|
||||
20.0,
|
||||
&Text::translate("selectWorld.experimental.details", &[]),
|
||||
true,
|
||||
ui::hovered(cursor, dx, dy, DETAILS, 20.0),
|
||||
);
|
||||
let [(px, py), (bx, by)] = footer_pair(w, h);
|
||||
for (fx, fy, key) in [(px, py, "gui.proceed"), (bx, by, "gui.back")] {
|
||||
ui::button(
|
||||
batch,
|
||||
atlas,
|
||||
fx,
|
||||
fy,
|
||||
BUTTON,
|
||||
20.0,
|
||||
&Text::translate(key, &[]),
|
||||
true,
|
||||
ui::hovered(cursor, fx, fy, BUTTON, 20.0),
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Two 150-wide buttons `COLUMN_SPACING` apart, centred.
|
||||
fn footer_pair(w: f32, h: f32) -> [(f32, f32); 2] {
|
||||
let x = ((w - (BUTTON * 2.0 + COLUMN_SPACING)) / 2.0).trunc();
|
||||
let y = h - 45.0;
|
||||
[(x, y), (x + BUTTON + COLUMN_SPACING, y)]
|
||||
}
|
||||
/// One centred footer button (`DetailsScreen`'s Back).
|
||||
fn footer_single(w: f32, h: f32) -> [(f32, f32); 1] {
|
||||
[(((w - BUTTON) / 2.0).trunc(), h - 45.0)]
|
||||
}
|
||||
@@ -569,10 +569,12 @@ fn classic_navigation_buttons_sound_even_without_a_game_action() {
|
||||
(Action::None, false)
|
||||
);
|
||||
assert_eq!(f.click_with_sound("Create New World"), (Action::None, true));
|
||||
assert_eq!(
|
||||
f.title.click(240.0, 55.0, (480.0, 262.0)),
|
||||
(Action::None, false)
|
||||
);
|
||||
// Create New World prepares the world generation first
|
||||
// (`CreateWorldScreen.openCreateWorldScreen`); the game opens the
|
||||
// screen once that is done.
|
||||
if f.title.click(240.0, 55.0, (480.0, 262.0)).0 == Action::None && f.title.preparing() {
|
||||
f.title.finish_preparing();
|
||||
}
|
||||
// The Game tab's game mode cycle button.
|
||||
let mode = f
|
||||
.title
|
||||
|
||||
Reference in new issue
Block a user