use crate::app::palette;
use bevy::prelude::*;
pub fn nav(keys: &ButtonInput<KeyCode>, selected: usize, len: usize) -> usize {
let mut at = selected;
if keys.just_pressed(KeyCode::KeyW) || keys.just_pressed(KeyCode::ArrowUp) {
at = (at + len - 1) % len;
}
if keys.just_pressed(KeyCode::KeyS) || keys.just_pressed(KeyCode::ArrowDown) {
at = (at + 1) % len;
}
at
}
pub fn nav_live(keys: &ButtonInput<KeyCode>, selected: usize, live: &[bool]) -> usize {
let up = keys.just_pressed(KeyCode::KeyW) || keys.just_pressed(KeyCode::ArrowUp);
let down = keys.just_pressed(KeyCode::KeyS) || keys.just_pressed(KeyCode::ArrowDown);
nav_live_steps(up, down, selected, live)
}
pub fn nav_live_steps(up: bool, down: bool, selected: usize, live: &[bool]) -> usize {
let len = live.len();
let step = if up {
len - 1 } else if down {
1
} else {
return first_live(selected, live).unwrap_or(selected);
};
let mut at = selected;
for _ in 0..len {
at = (at + step) % len;
if live[at] {
return at;
}
}
selected
}
fn first_live(from: usize, live: &[bool]) -> Option<usize> {
(0..live.len())
.map(|offset| (from + offset) % live.len())
.find(|&row| live[row])
}
pub fn left_right(keys: &ButtonInput<KeyCode>) -> Option<bool> {
if keys.just_pressed(KeyCode::KeyD) || keys.just_pressed(KeyCode::ArrowRight) {
Some(true)
} else if keys.just_pressed(KeyCode::KeyA) || keys.just_pressed(KeyCode::ArrowLeft) {
Some(false)
} else {
None
}
}
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_Y: f32 = 16.0;
pub fn screen_card() -> (Node, BackgroundColor, BorderColor, BoxShadow) {
(
Node {
flex_direction: FlexDirection::Column,
align_items: AlignItems::Center,
row_gap: Val::Px(ROW_GAP),
padding: UiRect::axes(Val::Px(22.0), Val::Px(CARD_PAD_Y)),
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 BAR_H: f32 = 52.0;
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()),
TextFont {
font_size: FontSize::Px(14.0),
..default()
},
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::GOLD.with_alpha(0.16)
} else {
Color::NONE
}
}
pub const ROW_H: f32 = 25.0;
pub const ROW_GAP: f32 = 2.0;
pub const HEADING_H: f32 = 20.0;
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(3.0)),
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(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: Color::srgba(0.0, 0.05, 0.12, 0.45),
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>,
ui: Query<Entity, With<M>>,
) {
settings.save();
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,
) {
for row in 0..count {
parent.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 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");
}
#[test]
fn nav_live_skips_hidden_rows() {
let live = [true, false, false, true, false];
let mut down = ButtonInput::<KeyCode>::default();
down.press(KeyCode::KeyS);
assert_eq!(nav_live(&down, 0, &live), 3);
assert_eq!(nav_live(&down, 3, &live), 0, "wraps past the dark tail");
let mut up = ButtonInput::<KeyCode>::default();
up.press(KeyCode::KeyW);
assert_eq!(nav_live(&up, 3, &live), 0);
assert_eq!(nav_live(&up, 0, &live), 3);
let idle = ButtonInput::<KeyCode>::default();
assert_eq!(nav_live(&idle, 1, &live), 3);
assert_eq!(nav_live(&idle, 3, &live), 3);
assert_eq!(nav_live(&down, 2, &[false, false, false]), 2);
}
#[test]
fn nav_wraps_both_ways() {
let mut keys = ButtonInput::<KeyCode>::default();
keys.press(KeyCode::KeyW);
assert_eq!(nav(&keys, 0, 5), 4, "up from the top wraps to the bottom");
let mut keys = ButtonInput::<KeyCode>::default();
keys.press(KeyCode::KeyS);
assert_eq!(nav(&keys, 4, 5), 0, "down from the bottom wraps to the top");
}
}