diff --git a/Readme.md b/Readme.md
index 17556a9..0791661 100644
--- a/Readme.md
+++ b/Readme.md
@@ -56,7 +56,7 @@ Check out the other [examples](https://github.com/gyscos/Cursive/tree/master/exa
-
+
diff --git a/doc/examples/mines.png b/doc/examples/mines.png
new file mode 100644
index 0000000..2328819
Binary files /dev/null and b/doc/examples/mines.png differ
diff --git a/examples/mines/Readme.md b/examples/mines/Readme.md
new file mode 100644
index 0000000..c05977b
--- /dev/null
+++ b/examples/mines/Readme.md
@@ -0,0 +1 @@
+This is a slightly larger example, showing an implementation of Minesweeper.
diff --git a/examples/mines/game.rs b/examples/mines/game.rs
new file mode 100644
index 0000000..072c012
--- /dev/null
+++ b/examples/mines/game.rs
@@ -0,0 +1,86 @@
+use cursive::vec::Vec2;
+use rand::{thread_rng, Rng};
+use std::cmp::max;
+
+#[derive(Clone, Copy)]
+pub struct Options {
+ pub size: Vec2,
+ pub mines: usize,
+}
+
+#[derive(Clone, Copy)]
+pub enum Cell {
+ Bomb,
+ Free(usize),
+}
+
+pub struct Board {
+ pub size: Vec2,
+ pub cells: Vec,
+}
+
+impl Board {
+ pub fn new(options: Options) -> Self {
+ let n_cells = options.size.x * options.size.y;
+ if options.mines > n_cells {
+ // Something is wrong here...
+ // Use different options instead.
+ return Board::new(Options {
+ size: options.size,
+ mines: n_cells,
+ });
+ }
+
+ let mut board = Board {
+ size: options.size,
+ cells: vec![Cell::Free(0); n_cells],
+ };
+
+ for _ in 0..options.mines {
+ // Find a free cell to put a bomb
+ let i = loop {
+ let i = thread_rng().gen_range(0, n_cells);
+
+ if let Cell::Bomb = board.cells[i] {
+ continue;
+ }
+
+ break i;
+ };
+
+ // We know we'll go through since that's how we picked i...
+ board.cells[i] = Cell::Bomb;
+ // Increase count on adjacent cells
+
+ let pos = Vec2::new(i % options.size.x, i / options.size.x);
+ for p in board.neighbours(pos) {
+ if let Some(&mut Cell::Free(ref mut n)) = board.get_mut(p) {
+ *n += 1;
+ }
+ }
+ }
+
+ board
+ }
+
+ fn get_mut(&mut self, pos: Vec2) -> Option<&mut Cell> {
+ self.cell_id(pos).map(move |i| &mut self.cells[i])
+ }
+
+ pub fn cell_id(&self, pos: Vec2) -> Option {
+ if pos < self.size {
+ Some(pos.x + pos.y * self.size.x)
+ } else {
+ None
+ }
+ }
+
+ pub fn neighbours(&self, pos: Vec2) -> Vec {
+ let pos_min = pos.saturating_sub((1, 1));
+ let pos_max = (pos + (2, 2)).or_min(self.size);
+ (pos_min.x..pos_max.x)
+ .flat_map(|x| (pos_min.y..pos_max.y).map(move |y| Vec2::new(x, y)))
+ .filter(|&p| p != pos)
+ .collect()
+ }
+}
diff --git a/examples/mines/main.rs b/examples/mines/main.rs
new file mode 100644
index 0000000..39a646f
--- /dev/null
+++ b/examples/mines/main.rs
@@ -0,0 +1,288 @@
+extern crate cursive;
+extern crate rand;
+
+mod game;
+
+use cursive::Cursive;
+use cursive::Printer;
+use cursive::direction::Direction;
+use cursive::event::{Event, EventResult, MouseButton, MouseEvent};
+use cursive::theme::{BaseColor, Color, ColorStyle};
+use cursive::vec::Vec2;
+use cursive::views::{Button, Dialog, LinearLayout, SelectView, Panel};
+
+fn main() {
+ let mut siv = Cursive::new();
+
+ siv.add_layer(
+ Dialog::new()
+ .title("Minesweeper")
+ .padding((2, 2, 1, 1))
+ .content(
+ LinearLayout::vertical()
+ .child(Button::new_raw(" New game ", show_options))
+ .child(Button::new_raw(" Best scores ", |s| {
+ s.add_layer(Dialog::info("Not yet!").title("Scores"))
+ }))
+ .child(Button::new_raw(" Exit ", |s| s.quit())),
+ ),
+ );
+
+ siv.run();
+}
+
+
+fn show_options(siv: &mut Cursive) {
+ siv.add_layer(
+ Dialog::new()
+ .title("Select difficulty")
+ .content(
+ SelectView::new()
+ .item(
+ "Easy: 8x8, 10 mines",
+ game::Options {
+ size: Vec2::new(8, 8),
+ mines: 10,
+ },
+ )
+ .item(
+ "Medium: 16x16, 40 mines",
+ game::Options {
+ size: Vec2::new(16, 16),
+ mines: 40,
+ },
+ )
+ .item(
+ "Difficult: 24x24, 99 mines",
+ game::Options {
+ size: Vec2::new(24, 24),
+ mines: 99,
+ },
+ )
+ .on_submit(|s, option| {
+ s.pop_layer();
+ new_game(s, *option);
+ }),
+ )
+ .button("Back", |s| s.pop_layer()),
+ );
+}
+
+#[derive(Clone, Copy, PartialEq)]
+enum Cell {
+ Visible(usize),
+ Flag,
+ Unknown,
+}
+
+struct BoardView {
+ // Actual board, unknown to the player.
+ board: game::Board,
+
+ // Visible board
+ overlay: Vec,
+
+ focused: Option,
+ missing_mines: usize,
+}
+
+impl BoardView {
+ pub fn new(options: game::Options) -> Self {
+ let overlay = vec![Cell::Unknown; options.size.x * options.size.y];
+ let board = game::Board::new(options);
+ BoardView {
+ board,
+ overlay,
+ focused: None,
+ missing_mines: options.mines,
+ }
+ }
+
+ fn get_cell(&self, mouse_pos: Vec2, offset: Vec2) -> Option {
+ mouse_pos
+ .checked_sub(offset)
+ .map(|pos| pos.map_x(|x| x / 2))
+ .and_then(|pos| {
+ if pos.fits_in(self.board.size) {
+ Some(pos)
+ } else {
+ None
+ }
+ })
+ }
+
+ fn flag(&mut self, pos: Vec2) {
+ if let Some(i) = self.board.cell_id(pos) {
+ let new_cell = match self.overlay[i] {
+ Cell::Unknown => Cell::Flag,
+ Cell::Flag => Cell::Unknown,
+ other => other,
+ };
+ self.overlay[i] = new_cell;
+ }
+ }
+
+ fn reveal(&mut self, pos: Vec2) -> EventResult {
+ if let Some(i) = self.board.cell_id(pos) {
+ if self.overlay[i] != Cell::Unknown {
+ return EventResult::Consumed(None);
+ }
+
+ // Action!
+ match self.board.cells[i] {
+ game::Cell::Bomb => {
+ return EventResult::with_cb(|s| {
+ s.add_layer(Dialog::text("BOOOM").button("Ok", |s| {
+ s.pop_layer();
+ s.pop_layer();
+ }));
+ })
+ }
+ game::Cell::Free(n) => {
+ self.overlay[i] = Cell::Visible(n);
+ if n == 0 {
+ // Reveal all surrounding cells
+ for p in self.board.neighbours(pos) {
+ self.reveal(p);
+ }
+ }
+ }
+ }
+ }
+ return EventResult::Consumed(None);
+ }
+
+ fn auto_reveal(&mut self, pos: Vec2) -> EventResult {
+ if let Some(i) = self.board.cell_id(pos) {
+ if let Cell::Visible(n) = self.overlay[i] {
+ // First: is every possible cell tagged?
+ let neighbours = self.board.neighbours(pos);
+ let tagged = neighbours
+ .iter()
+ .filter_map(|&pos| self.board.cell_id(pos))
+ .map(|i| self.overlay[i])
+ .filter(|&cell| cell == Cell::Flag)
+ .count();
+ if tagged != n {
+ return EventResult::Consumed(None);
+ }
+
+ for p in neighbours {
+ let result = self.reveal(p);
+ if result.has_callback() {
+ return result;
+ }
+ }
+ }
+ }
+
+ EventResult::Consumed(None)
+ }
+}
+
+impl cursive::view::View for BoardView {
+ fn draw(&self, printer: &Printer) {
+ for (i, cell) in self.overlay.iter().enumerate() {
+ let x = (i % self.board.size.x) * 2;
+ let y = i / self.board.size.x;
+
+ let text = match *cell {
+ Cell::Unknown => "[]",
+ Cell::Flag => "()",
+ Cell::Visible(n) => {
+ [" ", " 1", " 2", " 3", " 4", " 5", " 6", " 7", " 8"][n]
+ }
+ };
+
+ let color = match *cell {
+ Cell::Unknown => Color::RgbLowRes(3, 3, 3),
+ Cell::Flag => Color::RgbLowRes(4, 4, 2),
+ Cell::Visible(1) => Color::RgbLowRes(3, 5, 3),
+ Cell::Visible(2) => Color::RgbLowRes(5, 5, 3),
+ Cell::Visible(3) => Color::RgbLowRes(5, 4, 3),
+ Cell::Visible(4) => Color::RgbLowRes(5, 3, 3),
+ Cell::Visible(5) => Color::RgbLowRes(5, 2, 2),
+ Cell::Visible(6) => Color::RgbLowRes(5, 0, 1),
+ Cell::Visible(7) => Color::RgbLowRes(5, 0, 2),
+ Cell::Visible(8) => Color::RgbLowRes(5, 0, 3),
+ _ => Color::Dark(BaseColor::White),
+ };
+
+ printer.with_color(
+ ColorStyle::Custom {
+ back: color,
+ front: Color::Dark(BaseColor::Black),
+ },
+ |printer| printer.print((x, y), text),
+ );
+ }
+ }
+
+ fn take_focus(&mut self, _: Direction) -> bool {
+ true
+ }
+
+ fn on_event(&mut self, event: Event) -> EventResult {
+ match event {
+ Event::Mouse {
+ offset,
+ position,
+ event: MouseEvent::Press(btn),
+ } => {
+ // Get cell for position
+ if let Some(pos) = self.get_cell(position, offset) {
+ self.focused = Some(pos);
+ return EventResult::Consumed(None);
+ }
+ }
+ Event::Mouse {
+ offset,
+ position,
+ event: MouseEvent::Release(btn),
+ } => {
+ // Get cell for position
+ if let Some(pos) = self.get_cell(position, offset) {
+ if self.focused == Some(pos) {
+ // We got a click here!
+ match btn {
+ MouseButton::Left => return self.reveal(pos),
+ MouseButton::Right => {
+ self.flag(pos);
+ return EventResult::Consumed(None);
+ }
+ MouseButton::Middle => {
+ return self.auto_reveal(pos);
+ }
+ _ => (),
+ }
+ }
+
+ self.focused = None;
+ }
+ }
+ _ => (),
+ }
+
+ EventResult::Ignored
+ }
+
+ fn required_size(&mut self, _: Vec2) -> Vec2 {
+ self.board.size.map_x(|x| 2 * x)
+ }
+}
+
+fn new_game(siv: &mut Cursive, options: game::Options) {
+ let board = game::Board::new(options);
+
+ siv.add_layer(
+ Dialog::new()
+ .title("Minesweeper")
+ .content(
+ LinearLayout::horizontal()
+ .child(Panel::new(BoardView::new(options))),
+ )
+ .button("Quit game", |s| {
+ s.pop_layer();
+ }),
+ );
+}
diff --git a/src/backend/curses/n.rs b/src/backend/curses/n.rs
index 0c73cd5..29ea54a 100644
--- a/src/backend/curses/n.rs
+++ b/src/backend/curses/n.rs
@@ -424,7 +424,12 @@ fn initialize_keymap() -> HashMap {
// Then add some dynamic ones
for c in 1..26 {
- map.insert(c, Event::CtrlChar((b'a' - 1 + c as u8) as char));
+ let event = match c {
+ 9 => Event::Key(Key::Tab),
+ 10 => Event::Key(Key::Enter),
+ other => Event::CtrlChar((b'a' - 1 + other as u8) as char),
+ };
+ map.insert(c, event);
}
// Ncurses provides a F1 variable, but no modifiers
diff --git a/src/event.rs b/src/event.rs
index 4ac63b9..dc49d1c 100644
--- a/src/event.rs
+++ b/src/event.rs
@@ -76,7 +76,15 @@ impl EventResult {
pub fn is_consumed(&self) -> bool {
match *self {
EventResult::Consumed(_) => true,
- EventResult::Ignored => false,
+ _ => false,
+ }
+ }
+
+ /// Returns `true` if `self` contains a callback.
+ pub fn has_callback(&self) -> bool {
+ match *self {
+ EventResult::Consumed(Some(_)) => true,
+ _ => false,
}
}
diff --git a/src/views/button.rs b/src/views/button.rs
index 50ed911..3fac701 100644
--- a/src/views/button.rs
+++ b/src/views/button.rs
@@ -28,6 +28,15 @@ pub struct Button {
impl Button {
/// Creates a new button with the given content and callback.
pub fn new>(label: S, cb: F) -> Self
+ where
+ F: Fn(&mut Cursive) + 'static,
+ {
+ let label = label.into();
+ Self::new_raw(format!("<{}>", label), cb)
+ }
+
+ /// Creates a new button without angle brackets.
+ pub fn new_raw>(label: S, cb: F) -> Self
where
F: Fn(&mut Cursive) + 'static,
{
@@ -78,7 +87,7 @@ impl Button {
}
fn req_size(&self) -> Vec2 {
- Vec2::new(2 + self.label.width(), 1)
+ Vec2::new(self.label.width(), 1)
}
}
@@ -97,10 +106,10 @@ impl View for Button {
};
let offset =
- HAlign::Center.get_offset(self.label.len() + 2, printer.size.x);
+ HAlign::Center.get_offset(self.label.len(), printer.size.x);
printer.with_color(style, |printer| {
- printer.print((offset, 0), &format!("<{}>", self.label));
+ printer.print((offset, 0), &self.label);
});
}
diff --git a/src/xy.rs b/src/xy.rs
index 57955f4..35e2f20 100644
--- a/src/xy.rs
+++ b/src/xy.rs
@@ -21,6 +21,16 @@ impl XY {
XY::new(f(self.x), f(self.y))
}
+ /// Creates a new `XY` by applying `f` to `x`, and carrying `y` over.
+ pub fn map_x T>(self, f: F) -> Self {
+ XY::new(f(self.x), self.y)
+ }
+
+ /// Creates a new `XY` by applying `f` to `y`, and carrying `x` over.
+ pub fn map_y T>(self, f: F) -> Self {
+ XY::new(self.x, f(self.y))
+ }
+
/// Destructure self into a pair.
pub fn pair(self) -> (T, T) {
(self.x, self.y)
| |