use std::borrow::Cow;
use std::sync::Arc;
use all_is_cubes::block::AIR;
use all_is_cubes::block::{
Block, BlockAttributes,
Resolution::{self, *},
};
use all_is_cubes::camera;
use all_is_cubes::cgmath::Vector2;
use all_is_cubes::content::palette;
use all_is_cubes::drawing::embedded_graphics::{mono_font::iso_8859_1 as font, text::TextStyle};
use all_is_cubes::drawing::VoxelBrush;
use all_is_cubes::math::{Face6, FreeCoordinate, GridAab, GridCoordinate, GridVector, Rgba};
use all_is_cubes::space::{Space, SpaceBuilder, SpacePhysics};
use all_is_cubes::universe::{URef, Universe};
use crate::logo::logo_text;
use crate::vui::hud::HudInputs;
use crate::vui::options::pause_toggle_button;
use crate::vui::widgets;
use crate::vui::{
install_widgets, Align, Gravity, InstallVuiError, LayoutGrant, LayoutRequest, LayoutTree,
Widget, WidgetTree,
};
#[derive(Copy, Clone, Debug, Eq, PartialEq)]
pub(crate) struct UiSize {
size: Vector2<GridCoordinate>,
}
impl UiSize {
pub(crate) const DEPTH_BEHIND_VIEW_PLANE: GridCoordinate = 5;
pub fn new(viewport: camera::Viewport) -> Self {
let width = 25;
let height = ((FreeCoordinate::from(width) / viewport.nominal_aspect_ratio()).ceil()
as GridCoordinate)
.max(8);
let height = height / 2 * 2 + 1; Self {
size: Vector2::new(width, height),
}
}
pub(crate) fn space_bounds(&self) -> GridAab {
GridAab::from_lower_upper(
(0, 0, -Self::DEPTH_BEHIND_VIEW_PLANE),
(self.size.x, self.size.y, 5),
)
}
pub(crate) fn create_space(self) -> Space {
let bounds = self.space_bounds();
let Vector2 { x: w, y: h } = self.size;
let mut space = Space::builder(bounds)
.physics({
let mut physics = SpacePhysics::default();
physics.sky_color = palette::HUD_SKY;
physics
})
.build();
if false {
let mut add_frame = |z, color| {
let frame_block = Block::from(color);
space
.fill_uniform(GridAab::from_lower_size([0, 0, z], [w, h, 1]), frame_block)
.unwrap();
space
.fill_uniform(GridAab::from_lower_size([1, 1, z], [w - 2, h - 2, 1]), &AIR)
.unwrap();
};
add_frame(bounds.lower_bounds().z, Rgba::new(0.5, 0., 0., 1.));
add_frame(-1, Rgba::new(0.5, 0.5, 0.5, 1.));
add_frame(bounds.upper_bounds().z - 1, Rgba::new(0., 1., 1., 1.));
}
space
}
}
#[derive(Clone, Debug)]
pub(crate) struct PageInst {
tree: WidgetTree,
space: Option<URef<Space>>,
}
impl PageInst {
pub fn new(tree: WidgetTree) -> Self {
Self { tree, space: None }
}
pub fn get_or_create_space(&mut self, size: UiSize, universe: &mut Universe) -> URef<Space> {
if let Some(space) = self.space.as_ref() {
if space.read().unwrap().bounds() == size.space_bounds() {
return space.clone();
}
}
let space = universe.insert_anonymous(size.create_space());
space
.execute(
&install_widgets(LayoutGrant::new(size.space_bounds()), &self.tree)
.expect("layout/widget error"),
)
.expect("transaction error");
space
.try_modify(|space| {
space.fast_evaluate_light();
space.evaluate_light(10, |_| {});
})
.unwrap();
self.space = Some(space.clone());
space
}
}
fn page_modal_backdrop(foreground: WidgetTree) -> WidgetTree {
Arc::new(LayoutTree::Stack {
direction: Face6::PZ,
children: vec![
Arc::new(LayoutTree::Spacer(LayoutRequest {
minimum: GridVector::new(0, 0, UiSize::DEPTH_BEHIND_VIEW_PLANE + 2),
})),
LayoutTree::leaf(
widgets::Frame::with_block(Block::from(Rgba::new(0., 0., 0., 0.7)))
as Arc<dyn Widget>,
),
foreground,
],
})
}
pub(super) fn new_paused_widget_tree(hud_inputs: &HudInputs) -> WidgetTree {
page_modal_backdrop(LayoutTree::leaf(pause_toggle_button(hud_inputs)))
}
pub(super) fn new_about_widget_tree(
universe: &mut Universe,
hud_inputs: &HudInputs,
) -> Result<WidgetTree, InstallVuiError> {
let mut shrink =
|resolution: Resolution, large: WidgetTree| -> Result<Arc<dyn Widget>, InstallVuiError> {
let space = large.to_space(
SpaceBuilder::default().physics(SpacePhysics::DEFAULT_FOR_BLOCK),
Gravity::new(Align::Center, Align::Center, Align::Low),
)?;
Ok(Arc::new(widgets::Voxels::new(
space.bounds(),
universe.insert_anonymous(space),
resolution,
BlockAttributes::default(),
)))
};
fn heading(text: impl Into<Cow<'static, str>>) -> WidgetTree {
LayoutTree::leaf(Arc::new(widgets::LargeText {
text: text.into(),
font: || &font::FONT_9X15_BOLD,
brush: VoxelBrush::single(Block::from(Rgba::WHITE)),
text_style: TextStyle::default(),
}))
}
fn paragraph(text: impl Into<Cow<'static, str>>) -> WidgetTree {
LayoutTree::leaf(Arc::new(widgets::LargeText {
text: text.into(),
font: || &font::FONT_6X10,
brush: VoxelBrush::single(Block::from(Rgba::WHITE)),
text_style: TextStyle::default(),
}))
}
let controls_text = indoc::indoc! {"
W A S D movement
E C fly up/down (requires jetpack item)
Arrows turn
L toggle mouselook
0-9 select items on toolbar
Left mouse use first toolbar item
Right mouse use selected toolbar item
P toggle pause
Escape toggle pause; exit menu
"};
let about_text = String::from(indoc::indoc! {r#"
https://github.com/kpreid/all-is-cubes/
All is Cubes is a game-or-engine about building things out of voxels,
which I've been working on as a hobby since 2020. It's intended to be
a flexible and "self-hosting" system where everything can be edited
interactively (but it's not there yet, because I'm still building the
user interface architecture).
"#}) + env!("CARGO_PKG_VERSION");
let back_button = widgets::back_button(hud_inputs);
Ok(page_modal_backdrop(Arc::new(LayoutTree::Stack {
direction: Face6::NY,
children: vec![
LayoutTree::leaf(shrink(R8, LayoutTree::leaf(logo_text()))?),
back_button,
LayoutTree::leaf(shrink(R32, heading("Controls"))?),
LayoutTree::leaf(shrink(R32, paragraph(controls_text))?),
LayoutTree::leaf(shrink(R32, heading("About"))?),
LayoutTree::leaf(shrink(R32, paragraph(about_text))?),
],
})))
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn ui_size() {
let cases: Vec<([u32; 2], [i32; 2])> =
vec![([800, 600], [25, 19]), ([1000, 600], [25, 15])];
let mut failed = 0;
for (nominal_viewport, expected_size) in cases {
let actual_size =
UiSize::new(camera::Viewport::with_scale(1.0, nominal_viewport.into())).size;
let actual_size: [i32; 2] = actual_size.into();
if actual_size != expected_size {
println!("{nominal_viewport:?} expected to produce {expected_size:?}; got {actual_size:?}");
failed += 1;
}
}
if failed > 0 {
panic!("{failed} cases failed");
}
}
}