Implement camera panning in game_of_life example

This commit is contained in:
Héctor Ramón Jiménez 2020-04-30 04:12:13 +02:00
parent 611d9e399c
commit af95d3972e

View File

@ -136,13 +136,13 @@ impl Application for GameOfLife {
.align_items(Align::Center); .align_items(Align::Center);
let controls = Row::new() let controls = Row::new()
.padding(10)
.spacing(20) .spacing(20)
.push(playback_controls) .push(playback_controls)
.push(speed_controls); .push(speed_controls);
let content = Column::new() let content = Column::new()
.spacing(10) .spacing(10)
.padding(10)
.align_items(Align::Center) .align_items(Align::Center)
.push(self.grid.view().map(Message::Grid)) .push(self.grid.view().map(Message::Grid))
.push(controls); .push(controls);
@ -167,16 +167,27 @@ mod grid {
#[derive(Default)] #[derive(Default)]
pub struct Grid { pub struct Grid {
alive_cells: HashSet<(usize, usize)>, alive_cells: HashSet<(isize, isize)>,
mouse_pressed: bool, interaction: Option<Interaction>,
cache: canvas::Cache, cache: canvas::Cache,
translation: Vector,
}
#[derive(Debug, Clone, Copy)]
pub enum Message {
Populate { cell: (isize, isize) },
}
enum Interaction {
Drawing,
Panning { translation: Vector, start: Point },
} }
impl Grid { impl Grid {
fn with_neighbors( fn with_neighbors(
i: usize, i: isize,
j: usize, j: isize,
) -> impl Iterator<Item = (usize, usize)> { ) -> impl Iterator<Item = (isize, isize)> {
use itertools::Itertools; use itertools::Itertools;
let rows = i.saturating_sub(1)..=i.saturating_add(1); let rows = i.saturating_sub(1)..=i.saturating_add(1);
@ -188,7 +199,7 @@ mod grid {
pub fn tick(&mut self) { pub fn tick(&mut self) {
use itertools::Itertools; use itertools::Itertools;
let populated_neighbors: HashMap<(usize, usize), usize> = self let populated_neighbors: HashMap<(isize, isize), usize> = self
.alive_cells .alive_cells
.iter() .iter()
.flat_map(|&(i, j)| Self::with_neighbors(i, j)) .flat_map(|&(i, j)| Self::with_neighbors(i, j))
@ -230,53 +241,24 @@ mod grid {
.into() .into()
} }
fn populated_neighbors(&self, row: usize, column: usize) -> usize { fn populated_neighbors(&self, row: isize, column: isize) -> usize {
let with_neighbors = Self::with_neighbors(row, column); let with_neighbors = Self::with_neighbors(row, column);
let is_neighbor = |i: usize, j: usize| i != row || j != column; let is_neighbor = |i: isize, j: isize| i != row || j != column;
let is_populated = let is_populated =
|i: usize, j: usize| self.alive_cells.contains(&(i, j)); |i: isize, j: isize| self.alive_cells.contains(&(i, j));
with_neighbors with_neighbors
.filter(|&(i, j)| is_neighbor(i, j) && is_populated(i, j)) .filter(|&(i, j)| is_neighbor(i, j) && is_populated(i, j))
.count() .count()
} }
fn region(&self, size: Size) -> Rectangle { fn cell_at(&self, position: Point) -> Option<(isize, isize)> {
let width = let i = (position.y / CELL_SIZE as f32).ceil() as isize;
(size.width / CELL_SIZE as f32).floor() * CELL_SIZE as f32; let j = (position.x / CELL_SIZE as f32).ceil() as isize;
let height =
(size.height / CELL_SIZE as f32).floor() * CELL_SIZE as f32;
Rectangle { Some((i.saturating_sub(1), j.saturating_sub(1)))
x: (size.width - width) / 2.0,
y: (size.height - height) / 2.0,
width,
height,
}
} }
fn cell_at(
&self,
region: Rectangle,
position: Point,
) -> Option<(usize, usize)> {
if region.contains(position) {
let i = ((position.y - region.y) / CELL_SIZE as f32).ceil()
as usize;
let j = ((position.x - region.x) / CELL_SIZE as f32).ceil()
as usize;
Some((i.saturating_sub(1), j.saturating_sub(1)))
} else {
None
}
}
}
#[derive(Debug, Clone, Copy)]
pub enum Message {
Populate { cell: (usize, usize) },
} }
impl<'a> canvas::Program<Message> for Grid { impl<'a> canvas::Program<Message> for Grid {
@ -287,17 +269,15 @@ mod grid {
cursor: Cursor, cursor: Cursor,
) -> Option<Message> { ) -> Option<Message> {
if let Event::Mouse(mouse::Event::Input { if let Event::Mouse(mouse::Event::Input {
button: mouse::Button::Left, state: ButtonState::Released,
state, ..
}) = event }) = event
{ {
self.mouse_pressed = state == ButtonState::Pressed; self.interaction = None;
} }
let cursor_position = cursor.position_in(&bounds)?; let cursor_position = cursor.position_in(&bounds)?;
let cell = self.cell_at(cursor_position - self.translation)?;
let region = self.region(bounds.size());
let cell = self.cell_at(region, cursor_position)?;
let populate = if self.alive_cells.contains(&cell) { let populate = if self.alive_cells.contains(&cell) {
None None
@ -306,26 +286,53 @@ mod grid {
}; };
match event { match event {
Event::Mouse(mouse::Event::Input { Event::Mouse(mouse_event) => match mouse_event {
button: mouse::Button::Left, mouse::Event::Input {
.. button,
}) if self.mouse_pressed => populate, state: ButtonState::Pressed,
Event::Mouse(mouse::Event::CursorMoved { .. }) } => match button {
if self.mouse_pressed => mouse::Button::Left => {
{ self.interaction = Some(Interaction::Drawing);
populate
} populate
_ => None, }
mouse::Button::Right => {
self.interaction = Some(Interaction::Panning {
translation: self.translation,
start: cursor_position,
});
None
}
_ => None,
},
mouse::Event::CursorMoved { .. } => {
match self.interaction {
Some(Interaction::Drawing) => populate,
Some(Interaction::Panning {
translation,
start,
}) => {
self.translation =
translation + (cursor_position - start);
self.cache.clear();
None
}
_ => None,
}
}
_ => None,
},
} }
} }
fn draw(&self, bounds: Rectangle, cursor: Cursor) -> Vec<Geometry> { fn draw(&self, bounds: Rectangle, cursor: Cursor) -> Vec<Geometry> {
let region = self.region(bounds.size());
let cell_size = Size::new(1.0, 1.0); let cell_size = Size::new(1.0, 1.0);
let life = self.cache.draw(bounds.size(), |frame| { let life = self.cache.draw(bounds.size(), |frame| {
let background = let background = Path::rectangle(Point::ORIGIN, frame.size());
Path::rectangle(region.position(), region.size());
frame.fill( frame.fill(
&background, &background,
Color::from_rgb( Color::from_rgb(
@ -335,16 +342,25 @@ mod grid {
), ),
); );
let visible_rows = region.height as usize / CELL_SIZE; let first_row =
let visible_columns = region.width as usize / CELL_SIZE; (-self.translation.y / CELL_SIZE as f32).floor() as isize;
let first_column =
(-self.translation.x / CELL_SIZE as f32).floor() as isize;
let visible_rows =
(frame.height() / CELL_SIZE as f32).ceil() as isize;
let visible_columns =
(frame.width() / CELL_SIZE as f32).ceil() as isize;
frame.with_save(|frame| { frame.with_save(|frame| {
frame.translate(Vector::new(region.x, region.y)); frame.translate(self.translation);
frame.scale(CELL_SIZE as f32); frame.scale(CELL_SIZE as f32);
let cells = Path::new(|p| { let cells = Path::new(|p| {
for i in 0..visible_rows { for i in first_row..=(first_row + visible_rows) {
for j in 0..visible_columns { for j in
first_column..=(first_column + visible_columns)
{
if self.alive_cells.contains(&(i, j)) { if self.alive_cells.contains(&(i, j)) {
p.rectangle( p.rectangle(
Point::new(j as f32, i as f32), Point::new(j as f32, i as f32),
@ -361,11 +377,12 @@ mod grid {
let hovered_cell = { let hovered_cell = {
let mut frame = Frame::new(bounds.size()); let mut frame = Frame::new(bounds.size());
frame.translate(Vector::new(region.x, region.y)); frame.translate(self.translation);
frame.scale(CELL_SIZE as f32); frame.scale(CELL_SIZE as f32);
if let Some(cursor_position) = cursor.position_in(&bounds) { if let Some(cursor_position) = cursor.position_in(&bounds) {
if let Some((i, j)) = self.cell_at(region, cursor_position) if let Some((i, j)) =
self.cell_at(cursor_position - self.translation)
{ {
let interaction = Path::rectangle( let interaction = Path::rectangle(
Point::new(j as f32, i as f32), Point::new(j as f32, i as f32),
@ -393,12 +410,10 @@ mod grid {
bounds: Rectangle, bounds: Rectangle,
cursor: Cursor, cursor: Cursor,
) -> MouseCursor { ) -> MouseCursor {
let region = self.region(bounds.size()); match self.interaction {
Some(Interaction::Drawing) => MouseCursor::Crosshair,
match cursor.position_in(&bounds) { Some(Interaction::Panning { .. }) => MouseCursor::Grabbing,
Some(position) if region.contains(position) => { None if cursor.is_over(&bounds) => MouseCursor::Crosshair,
MouseCursor::Crosshair
}
_ => MouseCursor::default(), _ => MouseCursor::default(),
} }
} }