mod frame;
mod nav;
pub mod scroll_bar;
mod second_press;
pub use frame::{FOAM_DEPTH, FRAME_PX, Framed, ShoreCard, dress_cards, tide_line};
pub use nav::{
Buttons, CLOSERS, NUMBER_KEYS, Nav, enter, left_right, nav, number_pressed, screen_chosen,
spend, step,
};
#[cfg(test)]
pub use second_press::SECOND_PRESS_SECS;
pub use second_press::SecondPress;
use crate::app::palette;
use bevy::prelude::*;
pub fn display_font(px: f32) -> TextFont {
TextFont {
font: FontSource::Handle(crate::app::boot::DISPLAY_FONT),
font_size: FontSize::Px(px),
..default()
}
}
pub mod type_scale {
pub const TITLE: f32 = 54.0;
pub const DISPLAY: f32 = 34.0;
pub const HEADING: f32 = 24.0;
pub const ROW: f32 = 19.0;
pub const BODY: f32 = 17.0;
pub const FINE: f32 = 15.0;
}
pub mod layer {
pub const RESULTS: i32 = 10;
pub const BANNER: i32 = 15;
pub const CARD: i32 = 20;
pub const TOAST: i32 = 30;
const _: () = assert!(RESULTS < BANNER && BANNER < CARD && CARD < TOAST);
}
pub fn centred_overlay() -> Node {
Node {
position_type: PositionType::Absolute,
width: Val::Percent(100.0),
height: Val::Percent(100.0),
align_items: AlignItems::Center,
justify_content: JustifyContent::Center,
..default()
}
}
pub const CARD_PAD: f32 = FRAME_PX + 12.0;
pub fn screen_card() -> (ShoreCard, Node, BackgroundColor, BorderColor, BoxShadow) {
(
ShoreCard,
Node {
flex_direction: FlexDirection::Column,
align_items: AlignItems::Center,
row_gap: Val::Px(ROW_GAP),
min_height: Val::Px(0.0),
padding: UiRect::all(Val::Px(CARD_PAD)),
border: UiRect::all(Val::Px(1.0)),
border_radius: BorderRadius::all(Val::Px(16.0)),
..default()
},
BackgroundColor(palette::CARD_FILL),
BorderColor::all(palette::CARD_EDGE),
card_shadow(),
)
}
pub const HEADER_H: f32 = 42.0;
pub const UNDER_HEADER: f32 = HEADER_H + 4.0;
const BAR_AIR: f32 = 10.0;
pub const BAR_H: f32 = HEADER_H + BAR_AIR;
pub fn between_bars() -> Node {
Node {
position_type: PositionType::Absolute,
top: Val::Px(BAR_H),
bottom: Val::Px(BAR_H),
left: Val::Px(0.0),
right: Val::Px(0.0),
flex_direction: FlexDirection::Column,
align_items: AlignItems::Center,
justify_content: JustifyContent::Center,
row_gap: Val::Px(10.0),
..default()
}
}
pub fn heading(text: &str, first: bool) -> impl Bundle {
(
Text::new(text.to_string()),
display_font(type_scale::BODY),
TextColor(palette::GOLD.with_alpha(0.55)),
Node {
margin: UiRect::top(Val::Px(if first { 0.0 } else { 10.0 }))
.with_bottom(Val::Px(3.0))
.with_left(Val::Px(10.0)),
align_self: AlignSelf::FlexStart,
..default()
},
)
}
pub fn band(picked: bool) -> Color {
if picked {
palette::PICKED_WASH
} else {
Color::NONE
}
}
pub const ROW_H: f32 = 25.0;
pub const ROW_GAP: f32 = 2.0;
const ROW_PAD_Y: f32 = 3.0;
pub fn row_pitch(font: f32) -> f32 {
(font * LINE_HEIGHT + 2.0 * ROW_PAD_Y).max(ROW_H)
}
const HEADING_FONT: f32 = 15.0;
const HEADING_MARGIN: f32 = 6.0;
pub const LINE_HEIGHT: f32 = 1.2;
pub fn card_row() -> (Node, BackgroundColor) {
(
Node {
align_items: AlignItems::Center,
min_height: Val::Px(ROW_H),
padding: UiRect::axes(Val::Px(10.0), Val::Px(ROW_PAD_Y)),
border_radius: BorderRadius::all(Val::Px(7.0)),
..default()
},
BackgroundColor(Color::NONE),
)
}
#[derive(Clone, Copy, PartialEq, Eq)]
pub enum Half {
Label,
Value,
}
pub fn cell_ink(half: Half, picked: bool) -> Color {
match (half, picked) {
(Half::Label, true) => Color::WHITE,
(Half::Label, false) => palette::PARCHMENT.with_alpha(0.62),
(Half::Value, true) => palette::GOLD,
(Half::Value, false) => palette::PARCHMENT.with_alpha(0.92),
}
}
pub fn cell(width: f32, font_px: f32) -> (Node, Text, TextFont, TextLayout, TextColor) {
(
Node {
width: Val::Px(width),
flex_shrink: 0.0,
overflow: Overflow::clip_x(),
..default()
},
Text::new(""),
TextFont {
font_size: FontSize::Px(font_px),
..default()
},
TextLayout::no_wrap(),
TextColor(palette::IDLE_ROW),
)
}
pub fn card_shadow() -> BoxShadow {
BoxShadow::from(ShadowStyle {
color: palette::CARD_SHADOW,
x_offset: Val::Px(0.0),
y_offset: Val::Px(6.0),
spread_radius: Val::Px(0.0),
blur_radius: Val::Px(18.0),
})
}
pub fn despawn_marked<M: Component>(mut commands: Commands, ui: Query<Entity, With<M>>) {
for entity in &ui {
commands.entity(entity).despawn();
}
}
pub fn save_and_despawn<M: Component>(
mut commands: Commands,
settings: Res<crate::app::settings::GameSettings>,
caps: Res<crate::app::keycaps::KeyCaps>,
ui: Query<Entity, With<M>>,
) {
settings.save(&caps);
for entity in &ui {
commands.entity(entity).despawn();
}
}
pub fn spawn_rows<M: Component>(
parent: &mut ChildSpawnerCommands,
count: usize,
font_px: f32,
make: impl Fn(usize) -> M,
) {
parent
.spawn(Node {
flex_direction: FlexDirection::Column,
align_items: AlignItems::FlexStart,
row_gap: Val::Px(ROW_GAP),
..default()
})
.with_children(|column| {
for row in 0..count {
column.spawn((
make(row),
Text::new(""),
TextFont {
font_size: FontSize::Px(font_px),
..default()
},
TextColor(Color::WHITE),
));
}
});
}
pub fn set_text<T: std::ops::DerefMut<Target = String>>(text: &mut Mut<T>, value: &str) {
if text.as_str() != value {
value.clone_into(&mut ***text);
}
}
pub fn set_color(color: &mut Mut<TextColor>, target: Color) {
if color.0 != target {
color.0 = target;
}
}
pub fn set_bg(bg: &mut Mut<BackgroundColor>, target: Color) {
if bg.0 != target {
bg.0 = target;
}
}
pub fn set_shown(node: &mut Mut<Node>, shown: bool) {
let want = if shown { Display::Flex } else { Display::None };
if node.display != want {
node.display = want;
}
}
#[derive(bevy::ecs::system::SystemParam)]
pub struct Viewport<'w, 's> {
windows: Query<'w, 's, &'static Window>,
ui_scale: Res<'w, UiScale>,
}
impl Viewport<'_, '_> {
pub fn size(&self) -> Option<Vec2> {
let window = self.windows.single().ok()?;
Some(Vec2::new(window.width(), window.height()) / self.ui_scale.0.max(f32::EPSILON))
}
}
pub fn spacer(px: f32) -> Node {
Node {
height: Val::Px(px),
..default()
}
}
pub fn footnote(text: impl Into<String>) -> impl Bundle {
(
Text::new(text),
TextFont {
font_size: FontSize::Px(type_scale::BODY),
..default()
},
TextLayout::no_wrap(),
TextColor(palette::PARCHMENT.with_alpha(0.75)),
Node {
padding: UiRect::axes(Val::Px(12.0), Val::Px(3.0)),
border_radius: BorderRadius::all(Val::Px(11.0)),
..default()
},
BackgroundColor(palette::PILL_FILL),
)
}
pub fn heading_row(
panel: &mut bevy::ecs::relationship::RelatedSpawnerCommands<ChildOf>,
heading: &str,
icon: Option<&Handle<Image>>,
) {
panel
.spawn(Node {
flex_direction: FlexDirection::Row,
align_items: AlignItems::Center,
column_gap: Val::Px(8.0),
margin: UiRect::bottom(Val::Px(HEADING_MARGIN)),
width: Val::Percent(100.0),
..default()
})
.with_children(|head| {
if let Some(icon) = icon {
head.spawn((
ImageNode::new(icon.clone()).with_color(palette::HEADING_ICON),
Node {
width: Val::Px(22.0),
height: Val::Px(22.0),
..default()
},
));
}
head.spawn((
Text::new(heading),
display_font(HEADING_FONT),
TextColor(palette::IDLE_ROW),
));
head.spawn((
Node {
flex_grow: 1.0,
height: Val::Px(1.0),
margin: UiRect::left(Val::Px(8.0)),
..default()
},
BackgroundColor(palette::CARD_EDGE),
));
});
}
pub fn paint_row(selected: bool, line: &str, text: &mut Mut<Text>, color: &mut Mut<TextColor>) {
set_text(
text,
&format!("{} {line}", if selected { ">" } else { " " }),
);
let target = if selected {
palette::SELECTED_ROW
} else {
palette::IDLE_ROW
};
set_color(color, target);
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn rewriting_the_same_text_flags_nothing() {
let mut world = World::new();
let id = world
.spawn((Text::new("same"), TextColor(palette::IDLE_ROW)))
.id();
let flagged = |world: &mut World, write: &dyn Fn(&mut World)| {
world.clear_trackers();
let before = world.change_tick();
world.increment_change_tick();
write(world);
world
.entity(id)
.get_ref::<Text>()
.expect("spawned with text")
.last_changed()
.is_newer_than(before, world.change_tick())
};
assert!(
!flagged(&mut world, &|world| {
let mut text = world.get_mut::<Text>(id).expect("spawned with text");
set_text(&mut text, "same");
}),
"an identical string must not mark the text changed"
);
assert!(
flagged(&mut world, &|world| {
let mut text = world.get_mut::<Text>(id).expect("spawned with text");
set_text(&mut text, "different");
}),
"a real edit still has to mark it changed"
);
assert_eq!(world.entity(id).get::<Text>().expect("text").0, "different");
}
}