use iced::executor;
use iced::theme::{self, Theme};
use iced::time;
-use iced::widget::{button, column, container, row, slider, text};
+use iced::widget::{button, column, container, row, text};
use iced::{Alignment, Application, Command, Element, Length, Point, Subscription};
use rustc_hash::FxHashSet;
mask: Mask,
is_playing: bool,
bpm: usize,
+ is_looping: bool,
+ loop_len: usize,
+ step_num: usize,
}
#[derive(Debug, Clone)]
Map(map::Message),
Mask(mask::Message),
Tick(Instant),
- TogglePlayback,
Randomize,
Reset,
Clear,
Save,
+ TogglePlayback,
SpeedChanged(usize),
+ ToggleLoop,
+ LoopLength(usize),
}
impl Application for CellSeq {
(
Self {
bpm: 120,
+ loop_len: 16,
..Self::default()
},
Command::none(),
Message::Map(message) => self.map.update(message),
Message::Mask(message) => self.mask.update(message),
Message::Tick(_) => {
- return Command::perform(self.map.tick(), Message::Map);
+ let life = if self.step_num == self.loop_len && self.is_looping {
+ self.step_num = 0;
+ self.map.reset_loop()
+ } else {
+ self.step_num += 1;
+ self.map.tick()
+ };
+
+ self.map.update(map::Message::Ticked(life.clone()));
+ self.mask.update(mask::Message::Tick(life));
}
Message::TogglePlayback => {
self.is_playing = !self.is_playing;
+ if self.is_playing {
+ self.map.save()
+ }
}
- Message::SpeedChanged(bpm) => {
- self.bpm = bpm;
- }
+ Message::SpeedChanged(bpm) => self.bpm = bpm,
Message::Clear => self.map.clear(),
Message::Randomize => self.map.randomize(),
Message::Reset => self.map.reset(),
Message::Save => self.map.save(),
+ Message::ToggleLoop => {
+ self.is_looping = !self.is_looping;
+ if self.is_looping {
+ self.step_num = 0;
+ }
+ }
+ Message::LoopLength(len) => self.loop_len = len,
}
Command::none()
}
fn view(&self) -> Element<Message> {
- let bpm = self.bpm;
- let controls = view_controls(self.is_playing, bpm);
+ let controls = view_controls(
+ self.is_playing,
+ self.bpm,
+ self.is_looping,
+ self.loop_len,
+ self.step_num,
+ );
let map = row![
self.map.view().map(Message::Map),
self.mask.view().map(Message::Mask)
]
+ .align_items(Alignment::Center)
.width(Length::Fill)
- .spacing(40);
+ .spacing(40)
+ .padding(20);
let content = column![controls, map,];
}
}
-fn view_controls<'a>(is_playing: bool, bpm: usize) -> Element<'a, Message> {
- let playback_controls =
- row![button(if is_playing { "pause" } else { "play" }).on_press(Message::TogglePlayback),]
- .spacing(10);
+fn view_controls<'a>(
+ is_playing: bool,
+ bpm: usize,
+ is_looping: bool,
+ loop_len: usize,
+ step_num: usize,
+) -> Element<'a, Message> {
+ let playback_controls = row![
+ button(if is_playing { "pause" } else { "play" }).on_press(Message::TogglePlayback),
+ button(if is_looping { "free" } else { "loop" }).on_press(Message::ToggleLoop),
+ button("-").on_press(Message::LoopLength(loop_len.saturating_sub(1))),
+ text(if is_looping {
+ format!("{step_num}/{loop_len}")
+ } else {
+ format!("{loop_len}")
+ }),
+ button("+").on_press(Message::LoopLength(loop_len.saturating_add(1)))
+ ]
+ .spacing(10);
let speed_controls = row![
- slider(1.0..=1000.0, bpm as f32, |m| Message::SpeedChanged(
- m.round() as usize
- )),
+ button("<<").on_press(Message::SpeedChanged(bpm.saturating_sub(5))),
+ button("<").on_press(Message::SpeedChanged(bpm.saturating_sub(1))),
text(format!("{bpm}")).size(16),
+ button(">").on_press(Message::SpeedChanged(bpm.saturating_add(1))),
+ button(">>").on_press(Message::SpeedChanged(bpm.saturating_add(1))),
]
.width(Length::Fill)
.align_items(Alignment::Center)
.spacing(10);
- row![
- playback_controls,
- speed_controls,
+ let other_controls = row![
button("save").on_press(Message::Save),
button("reset")
.on_press(Message::Reset)
.on_press(Message::Clear)
.style(theme::Button::Destructive),
]
- .padding(10)
- .spacing(20)
+ .width(Length::Fill)
.align_items(Alignment::Center)
- .into()
+ .spacing(10);
+
+ row![playback_controls, speed_controls, other_controls]
+ .padding(10)
+ .spacing(40)
+ .align_items(Alignment::Center)
+ .into()
}
use itertools::Itertools;
use rand::random;
use rustc_hash::FxHashMap;
-use std::{fmt::Debug, future::Future};
+use std::fmt::Debug;
#[derive(Default, Debug)]
pub struct Map {
}
impl Map {
- pub fn tick(&self) -> impl Future<Output = Message> {
+ pub fn reset_loop(&mut self) -> CellMap {
+ self.seed.clone()
+ }
+
+ pub fn tick(&self) -> CellMap {
let mut life = self.cells.clone();
let mut counts = FxHashMap::default();
- let tick = tokio::task::spawn_blocking(move || {
- for cell in &life {
- counts.entry(*cell).or_insert(0);
+ for cell in &life {
+ counts.entry(*cell).or_insert(0);
- for neighbor in Cell::neighbors(*cell) {
- let amount = counts.entry(neighbor).or_insert(0);
+ for neighbor in Cell::neighbors(*cell) {
+ let amount = counts.entry(neighbor).or_insert(0);
- *amount += 1;
- }
+ *amount += 1;
}
+ }
- for (cell, amount) in counts.iter() {
- match amount {
- 2 => {}
- 3 => {
- life.insert(*cell);
- }
- _ => {
- life.remove(cell);
- }
+ for (cell, amount) in counts.iter() {
+ match amount {
+ 2 => {}
+ 3 => {
+ life.insert(*cell);
+ }
+ _ => {
+ life.remove(cell);
}
}
+ }
- life
- });
-
- async move { Message::Ticked(tick.await.unwrap()) }
+ life
}
pub fn update(&mut self, message: Message) {
self.seed = self.cells.clone();
self.life_cache.clear();
}
-
- pub fn contains(&self, cell: &Cell) -> bool {
- self.cells.contains(cell)
- }
}
impl Program<Message> for Map {
let cell = Cell::at(position);
return (
event::Status::Captured,
- if self.contains(&cell) {
+ if self.cells.contains(&cell) {
Some(Message::Unpopulate(cell))
} else {
Some(Message::Populate(cell))
(0..24)
.cartesian_product(0..24)
- .filter(|x| self.contains(&Cell { i: x.1, j: x.0 }))
+ .filter(|x| self.cells.contains(&Cell { i: x.1, j: x.0 }))
.for_each(|x| {
frame.fill_rectangle(
Point::new(x.0 as f32, x.1 as f32),
#[derive(Default, Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub struct Note {
value: usize,
- action: State,
+ state: State,
+}
+
+impl Note {
+ pub fn is_on(&self) -> bool {
+ match self.state {
+ State::On => true,
+ State::Off => false,
+ }
+ }
+
+ pub fn switch(&mut self) {
+ self.state = match self.state {
+ State::On => State::Off,
+ State::Off => State::On,
+ }
+ }
}
#[derive(Default, Debug)]
}
impl Mask {
- pub fn contains(&self, cell: &Cell) -> bool {
- self.cells.contains_key(cell)
- }
-
- pub fn check(&mut self, cell: Cell) {
- self.cells.insert(cell, Note::default());
- }
-
- pub fn uncheck(&mut self, cell: Cell) {
- let _ = self.cells.remove(&cell);
- }
-
- pub fn get_note(&self, cell: &Cell) -> Option<&Note> {
- self.cells.get(cell)
- }
-
- pub fn cells(&self) -> impl Iterator<Item = &Cell> {
- self.cells.keys()
- }
-
pub fn update(&mut self, message: Message) {
match message {
- Message::Check(cell) => self.check(cell),
- Message::Uncheck(cell) => self.uncheck(cell),
- Message::Tick(life) => {}
+ Message::Check(cell) => {
+ self.cells.insert(cell, Note::default());
+ self.mask_cache.clear()
+ }
+ Message::Uncheck(cell) => {
+ self.cells.remove(&cell);
+ self.mask_cache.clear();
+ }
+ Message::Tick(life) => {
+ for cell in life.iter() {
+ if self.cells.contains_key(cell) {
+ let note = self.cells.entry(*cell).or_default();
+ note.switch()
+ }
+ }
+
+ self.mask_cache.clear();
+ }
+ }
+ }
+
+ pub fn is_on(&self, cell: &Cell) -> bool {
+ if let Some(note) = self.cells.get(cell) {
+ note.is_on()
+ } else {
+ false
}
}
(0..24)
.cartesian_product(0..24)
- .filter(|x| self.contains(&Cell { i: x.1, j: x.0 }))
+ .filter(|x| self.cells.contains_key(&Cell { i: x.1, j: x.0 }))
.for_each(|x| {
- frame.fill_rectangle(
- Point::new(x.0 as f32, x.1 as f32),
- Size::UNIT,
- Color::WHITE,
- );
+ let color = if self.is_on(&Cell { i: x.0, j: x.1 }) {
+ Color::from_rgb8(0xF0, 0xC0, 0xC0)
+ } else {
+ Color::WHITE
+ };
+
+ frame.fill_rectangle(Point::new(x.0 as f32, x.1 as f32), Size::UNIT, color);
})
});
})]
let cell = Cell::at(position);
return (
event::Status::Captured,
- if self.contains(&cell) {
+ if self.cells.contains_key(&cell) {
Some(Message::Uncheck(cell))
} else {
Some(Message::Check(cell))