use crate::State;
use cursive::view::Scrollable;
use itertools::Itertools;
use std::sync::{
atomic::{AtomicBool, Ordering},
Arc, Mutex,
};
lazy_static::lazy_static! {
static ref KEY_COMMANDS: Vec<KeyCommandGroup> = {
vec![
KeyCommandGroup::new(
"camera",
vec![
KeyCommand::new(
vec![Key::Up, Key::Char('k')],
Action::PanUp,
"pan up"
),
KeyCommand::new(
vec![Key::Down, Key::Char('j')],
Action::PanDown,
"pan down"
),
KeyCommand::new(
vec![Key::Left, Key::Char('h')],
Action::PanLeft,
"pan left"
),
KeyCommand::new(
vec![Key::Right, Key::Char('l')],
Action::PanRight,
"pan right"
),
KeyCommand::new(
vec![Key::ShiftUp, Key::Char('K')],
Action::PanUpSmall,
"pan up (small)"
),
KeyCommand::new(
vec![Key::ShiftDown, Key::Char('J')],
Action::PanDownSmall,
"pan down (small)"
),
KeyCommand::new(
vec![Key::ShiftLeft, Key::Char('H')],
Action::PanLeftSmall,
"pan left (small)"
),
KeyCommand::new(
vec![Key::ShiftRight, Key::Char('L')],
Action::PanRightSmall,
"pan right (small)"
),
KeyCommand::new(
vec![Key::Char('[')],
Action::IncreaseScale,
"zoom out"
),
KeyCommand::new(
vec![Key::Char(']')],
Action::DecreaseScale,
"zoom in"
),
KeyCommand::new(
vec![Key::Char('f')],
Action::ZoomToFit,
"zoom to fit (might be slow)"
),
]
),
KeyCommandGroup::new(
"simuate",
vec![
KeyCommand::new(
vec![Key::Enter],
Action::ToggleSimulation,
"start/stop simulation"
),
KeyCommand::new(
vec![Key::Char(' ')],
Action::StepOneGeneration,
"advance one generation"
),
KeyCommand::new(
vec![Key::Tab],
Action::Step,
"advance <step> generations"
),
KeyCommand::new(
vec![Key::Char('-')],
Action::DecreaseStep,
"decrease step size"
),
KeyCommand::new(
vec![Key::Char('=')],
Action::IncreaseStep,
"increase step size"
),
KeyCommand::new(
vec![Key::Char('9')],
Action::DecreaseDelay,
"decrease frame delay"
),
KeyCommand::new(
vec![Key::Char('0')],
Action::IncreaseDelay,
"increase frame delay"
),
]
),
KeyCommandGroup::new(
"other",
vec![
KeyCommand::new(
vec![Key::Char('q')],
Action::Quit,
"quit"
),
KeyCommand::new(
vec![Key::Char('?')],
Action::ShowHelp,
"toggle help"
),
]
),
]
};
}
#[derive(Clone, Copy, Debug)]
pub enum Key {
Char(char),
Enter,
Tab,
Up,
Down,
Left,
Right,
ShiftUp,
ShiftDown,
ShiftLeft,
ShiftRight,
}
impl Key {
fn into_event(self) -> cursive::event::Event {
match self {
Key::Char(c) => cursive::event::Event::Char(c),
Key::Enter => cursive::event::Event::Key(cursive::event::Key::Enter),
Key::Tab => cursive::event::Event::Key(cursive::event::Key::Tab),
Key::Up => cursive::event::Event::Key(cursive::event::Key::Up),
Key::Down => cursive::event::Event::Key(cursive::event::Key::Down),
Key::Left => cursive::event::Event::Key(cursive::event::Key::Left),
Key::Right => cursive::event::Event::Key(cursive::event::Key::Right),
Key::ShiftUp => cursive::event::Event::Shift(cursive::event::Key::Up),
Key::ShiftDown => cursive::event::Event::Shift(cursive::event::Key::Down),
Key::ShiftLeft => cursive::event::Event::Shift(cursive::event::Key::Left),
Key::ShiftRight => cursive::event::Event::Shift(cursive::event::Key::Right),
}
}
fn display(self) -> String {
match self {
Key::Char(' ') => "<space>".to_owned(),
Key::Char(c) => format!("{}", c),
Key::Enter => "<enter>".to_owned(),
Key::Tab => "<tab>".to_owned(),
Key::Up => "↑".to_owned(),
Key::Down => "↓".to_owned(),
Key::Left => "←".to_owned(),
Key::Right => "→".to_owned(),
Key::ShiftUp => "<shift> ↑".to_owned(),
Key::ShiftDown => "<shift> ↓".to_owned(),
Key::ShiftLeft => "<shift> ←".to_owned(),
Key::ShiftRight => "<shift> →".to_owned(),
}
}
}
#[derive(Clone, Copy, Debug)]
pub enum Action {
PanLeft,
PanRight,
PanUp,
PanDown,
PanLeftSmall,
PanRightSmall,
PanUpSmall,
PanDownSmall,
IncreaseStep,
DecreaseStep,
IncreaseScale,
DecreaseScale,
ToggleSimulation,
StepOneGeneration,
Step,
IncreaseDelay,
DecreaseDelay,
Quit,
ShowHelp,
ZoomToFit,
}
#[derive(Clone, Debug)]
pub struct KeyCommand {
keys: Vec<Key>,
action: Action,
description: &'static str,
}
impl KeyCommand {
fn new(keys: Vec<Key>, action: Action, description: &'static str) -> Self {
Self {
keys,
action,
description,
}
}
}
pub struct KeyCommandGroup {
name: &'static str,
key_commands: Vec<KeyCommand>,
}
impl KeyCommandGroup {
fn new(name: &'static str, key_commands: Vec<KeyCommand>) -> Self {
Self { name, key_commands }
}
}
const MOVEMENT_FACTOR: u64 = 4;
const MIN_SCALE: u64 = 1;
const MAX_SCALE: u64 = 1 << 48;
const MAX_STEP_LOG_2: u8 = 48;
fn pan_down(center: &Arc<Mutex<(i64, i64)>>, scale: &Arc<Mutex<u64>>) {
let mut center = center.lock().unwrap();
center.1 += (MOVEMENT_FACTOR * *scale.lock().unwrap()) as i64;
}
fn pan_up(center: &Arc<Mutex<(i64, i64)>>, scale: &Arc<Mutex<u64>>) {
let mut center = center.lock().unwrap();
center.1 -= (MOVEMENT_FACTOR * *scale.lock().unwrap()) as i64;
}
fn pan_left(center: &Arc<Mutex<(i64, i64)>>, scale: &Arc<Mutex<u64>>) {
let mut center = center.lock().unwrap();
center.0 -= (MOVEMENT_FACTOR * *scale.lock().unwrap()) as i64;
}
fn pan_right(center: &Arc<Mutex<(i64, i64)>>, scale: &Arc<Mutex<u64>>) {
let mut center = center.lock().unwrap();
center.0 += (MOVEMENT_FACTOR * *scale.lock().unwrap()) as i64;
}
fn pan_down_small(center: &Arc<Mutex<(i64, i64)>>, scale: &Arc<Mutex<u64>>) {
let mut center = center.lock().unwrap();
center.1 += *scale.lock().unwrap() as i64;
}
fn pan_up_small(center: &Arc<Mutex<(i64, i64)>>, scale: &Arc<Mutex<u64>>) {
let mut center = center.lock().unwrap();
center.1 -= *scale.lock().unwrap() as i64;
}
fn pan_left_small(center: &Arc<Mutex<(i64, i64)>>, scale: &Arc<Mutex<u64>>) {
let mut center = center.lock().unwrap();
center.0 -= *scale.lock().unwrap() as i64;
}
fn pan_right_small(center: &Arc<Mutex<(i64, i64)>>, scale: &Arc<Mutex<u64>>) {
let mut center = center.lock().unwrap();
center.0 += *scale.lock().unwrap() as i64;
}
fn step_one_generation(life: &Arc<Mutex<smeagol::Life>>) {
life.lock().unwrap().step();
}
fn toggle_simulation(is_running: &Arc<AtomicBool>) {
is_running.store(!is_running.load(Ordering::SeqCst), Ordering::SeqCst);
}
fn increase_scale(scale: &Arc<Mutex<u64>>) {
let mut scale = scale.lock().unwrap();
if *scale < MAX_SCALE {
*scale <<= 1;
}
}
fn decrease_scale(scale: &Arc<Mutex<u64>>) {
let mut scale = scale.lock().unwrap();
if *scale > MIN_SCALE {
*scale >>= 1;
}
}
fn zoom_to_fit(
life: &Arc<Mutex<smeagol::Life>>,
center: &Arc<Mutex<(i64, i64)>>,
scale: &Arc<Mutex<u64>>,
) {
let life = life.lock().unwrap();
if life.population() > 0 {
let (output_width, output_height) = term_size::dimensions().unwrap();
let bbox = life.bounding_box().unwrap();
let x_min = bbox.upper_left().x;
let y_min = bbox.upper_left().y;
let x_max = bbox.lower_right().x;
let y_max = bbox.lower_right().y;
let new_center = ((x_min + x_max) / 2, (y_min + y_max) / 2);
let width = (x_max - x_min + 1) as f64;
let height = (y_max - y_min + 1) as f64;
let new_scale = ((width / ((output_width as f64) * 2.))
.ceil()
.max((height / (((output_height - 1) as f64) * 4.)).ceil())
as u64)
.next_power_of_two();
*center.lock().unwrap() = new_center;
*scale.lock().unwrap() = new_scale;
} else {
*center.lock().unwrap() = (0, 0);
*scale.lock().unwrap() = 1;
}
}
fn increase_step(life: &Arc<Mutex<smeagol::Life>>) {
let mut life = life.lock().unwrap();
let step_log_2 = life.step_log_2();
if step_log_2 < MAX_STEP_LOG_2 {
life.set_step_log_2(step_log_2 + 1);
}
}
fn decrease_step(life: &Arc<Mutex<smeagol::Life>>) {
let mut life = life.lock().unwrap();
let step_log_2 = life.step_log_2();
if step_log_2 > 0 {
life.set_step_log_2(step_log_2 - 1);
}
}
fn increase_delay(delay: &Arc<Mutex<u64>>) {
let mut delay = delay.lock().unwrap();
if *delay < (1 << 10) {
*delay <<= 1;
}
}
fn decrease_delay(delay: &Arc<Mutex<u64>>) {
let mut delay = delay.lock().unwrap();
if *delay > 1 {
*delay >>= 1;
}
}
fn quit(siv: &mut cursive::Cursive) {
siv.quit()
}
fn show_help(siv: &mut cursive::Cursive) {
let mut stack = siv.find_id::<cursive::views::StackView>("stack").unwrap();
if stack
.get(cursive::views::LayerPosition::FromBack(1))
.is_some()
{
stack.pop_layer();
} else {
let mut help = cursive::views::LinearLayout::vertical();
help.add_child(
cursive::views::TextView::new(format!("smeagol {}", clap::crate_version!())).center(),
);
help.add_child(cursive::views::TextView::new(clap::crate_authors!()).center());
let mut groups = cursive::views::LinearLayout::horizontal();
for key_command_group in KEY_COMMANDS.iter() {
let mut group = cursive::views::LinearLayout::vertical();
group.add_child(cursive::views::TextView::new(" "));
group.add_child(cursive::views::TextView::new(key_command_group.name).center());
group.add_child(cursive::views::TextView::new(" "));
let mut help_list = cursive::views::ListView::new();
for key_command in &key_command_group.key_commands {
let label = key_command
.keys
.iter()
.map(|key| key.display())
.intersperse(", ".to_owned())
.collect::<String>();
help_list.add_child(
&label,
cursive::views::TextView::new(key_command.description),
);
}
group.add_child(help_list);
groups.add_child(cursive::views::PaddedView::new(((1, 1), (0, 0)), group));
}
help.add_child(groups);
stack.add_layer(cursive::views::IdView::new(
"help",
cursive::views::PaddedView::new(
((2, 2), (1, 1)),
help.scrollable().scroll_x(true).scroll_y(true),
),
));
}
}
pub fn setup_key_commands(siv: &mut cursive::Cursive, state: &State) {
for key_command_group in KEY_COMMANDS.iter() {
for key_command in key_command_group.key_commands.iter() {
for &key in &key_command.keys {
match key_command.action {
Action::PanDown => {
siv.add_global_callback(
key.into_event(),
enclose!((state) move |_: &mut cursive::Cursive| {
pan_down(&state.center, &state.scale)
}),
);
}
Action::PanUp => {
siv.add_global_callback(
key.into_event(),
enclose!((state) move |_: &mut cursive::Cursive| {
pan_up(&state.center, &state.scale)
}),
);
}
Action::PanLeft => {
siv.add_global_callback(
key.into_event(),
enclose!((state) move |_: &mut cursive::Cursive| {
pan_left(&state.center, &state.scale)
}),
);
}
Action::PanRight => {
siv.add_global_callback(
key.into_event(),
enclose!((state) move |_: &mut cursive::Cursive| {
pan_right(&state.center, &state.scale)
}),
);
}
Action::PanDownSmall => {
siv.add_global_callback(
key.into_event(),
enclose!((state) move |_: &mut cursive::Cursive| {
pan_down_small(&state.center, &state.scale)
}),
);
}
Action::PanUpSmall => {
siv.add_global_callback(
key.into_event(),
enclose!((state) move |_: &mut cursive::Cursive| {
pan_up_small(&state.center, &state.scale)
}),
);
}
Action::PanLeftSmall => {
siv.add_global_callback(
key.into_event(),
enclose!((state) move |_: &mut cursive::Cursive| {
pan_left_small(&state.center, &state.scale)
}),
);
}
Action::PanRightSmall => {
siv.add_global_callback(
key.into_event(),
enclose!((state) move |_: &mut cursive::Cursive| {
pan_right_small(&state.center, &state.scale)
}),
);
}
Action::IncreaseScale => {
siv.add_global_callback(
key.into_event(),
enclose!((state) move |_: &mut cursive::Cursive| {
increase_scale(&state.scale)
}),
);
}
Action::DecreaseScale => {
siv.add_global_callback(
key.into_event(),
enclose!((state) move |_: &mut cursive::Cursive| {
decrease_scale(&state.scale)
}),
);
}
Action::ZoomToFit => {
siv.add_global_callback(
key.into_event(),
enclose!((state) move |_: &mut cursive::Cursive| {
zoom_to_fit(&state.life, &state.center, &state.scale)
}),
);
}
Action::IncreaseStep => {
siv.add_global_callback(
key.into_event(),
enclose!((state) move |_: &mut cursive::Cursive| {
increase_step(&state.life)
}),
);
}
Action::DecreaseStep => {
siv.add_global_callback(
key.into_event(),
enclose!((state) move |_: &mut cursive::Cursive| {
decrease_step(&state.life)
}),
);
}
Action::StepOneGeneration => {
siv.add_global_callback(
key.into_event(),
enclose!((state) move |_: &mut cursive::Cursive| {
step_one_generation(&state.life)
}),
);
}
Action::Step => {
siv.add_global_callback(
key.into_event(),
enclose!(() move |_: &mut cursive::Cursive| {
}),
);
}
Action::ToggleSimulation => {
siv.add_global_callback(
key.into_event(),
enclose!((state) move |_: &mut cursive::Cursive| {
toggle_simulation(&state.is_running)
}),
);
}
Action::IncreaseDelay => {
siv.add_global_callback(
key.into_event(),
enclose!((state) move |_: &mut cursive::Cursive| {
increase_delay(&state.delay_millis)
}),
);
}
Action::DecreaseDelay => {
siv.add_global_callback(
key.into_event(),
enclose!((state) move |_: &mut cursive::Cursive| {
decrease_delay(&state.delay_millis)
}),
);
}
Action::Quit => {
siv.add_global_callback(key.into_event(), quit);
}
Action::ShowHelp => {
siv.add_global_callback(key.into_event(), show_help);
}
}
}
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn change_scale() {
let scale = Arc::new(Mutex::new(8));
increase_scale(&scale);
assert_eq!(*scale.lock().unwrap(), 16);
decrease_scale(&scale);
assert_eq!(*scale.lock().unwrap(), 8);
let min_scale = Arc::new(Mutex::new(MIN_SCALE));
decrease_scale(&min_scale);
assert_eq!(*min_scale.lock().unwrap(), MIN_SCALE);
let max_scale = Arc::new(Mutex::new(MAX_SCALE));
increase_scale(&max_scale);
assert_eq!(*max_scale.lock().unwrap(), MAX_SCALE);
}
#[test]
fn pan() {
let center = Arc::new(Mutex::new((0, 0)));
let scale = Arc::new(Mutex::new(4));
pan_down(¢er, &scale);
assert_eq!(*center.lock().unwrap(), (0, 4 * MOVEMENT_FACTOR as i64));
pan_up(¢er, &scale);
assert_eq!(*center.lock().unwrap(), (0, 0));
pan_right(¢er, &scale);
assert_eq!(*center.lock().unwrap(), (4 * MOVEMENT_FACTOR as i64, 0));
pan_left(¢er, &scale);
assert_eq!(*center.lock().unwrap(), (0, 0));
}
#[test]
fn dummy_setup_key_commands() {
let mut siv = cursive::Cursive::dummy();
let life = smeagol::Life::new();
let state = State::new_centered(life, 20, 20);
setup_key_commands(&mut siv, &state);
}
}