From 0de4af3bca8955633158d0f21558334fc6c0ff2a Mon Sep 17 00:00:00 2001 From: Huck Boles Date: Wed, 21 Jun 2023 18:13:03 -0500 Subject: [PATCH] added: map mask interaction --- src/lib.rs | 93 ++++++++++++++++++++++++++++++++++++++++------------- src/map.rs | 52 ++++++++++++++---------------- src/mask.rs | 83 +++++++++++++++++++++++++++++------------------ 3 files changed, 147 insertions(+), 81 deletions(-) diff --git a/src/lib.rs b/src/lib.rs index 814651b..ac87d2b 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -1,7 +1,7 @@ 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; @@ -54,6 +54,9 @@ pub struct CellSeq { mask: Mask, is_playing: bool, bpm: usize, + is_looping: bool, + loop_len: usize, + step_num: usize, } #[derive(Debug, Clone)] @@ -61,12 +64,14 @@ pub enum Message { Map(map::Message), Mask(mask::Message), Tick(Instant), - TogglePlayback, Randomize, Reset, Clear, Save, + TogglePlayback, SpeedChanged(usize), + ToggleLoop, + LoopLength(usize), } impl Application for CellSeq { @@ -79,6 +84,7 @@ impl Application for CellSeq { ( Self { bpm: 120, + loop_len: 16, ..Self::default() }, Command::none(), @@ -94,18 +100,35 @@ impl Application for CellSeq { 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() @@ -120,14 +143,21 @@ impl Application for CellSeq { } fn view(&self) -> Element { - 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,]; @@ -142,24 +172,38 @@ impl Application for CellSeq { } } -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) @@ -171,8 +215,13 @@ fn view_controls<'a>(is_playing: bool, bpm: usize) -> Element<'a, Message> { .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() } diff --git a/src/map.rs b/src/map.rs index 7bb1060..2cc07ff 100644 --- a/src/map.rs +++ b/src/map.rs @@ -11,7 +11,7 @@ use super::*; 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 { @@ -28,37 +28,37 @@ pub enum Message { } impl Map { - pub fn tick(&self) -> impl Future { + 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) { @@ -109,10 +109,6 @@ impl Map { self.seed = self.cells.clone(); self.life_cache.clear(); } - - pub fn contains(&self, cell: &Cell) -> bool { - self.cells.contains(cell) - } } impl Program for Map { @@ -130,7 +126,7 @@ impl Program 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)) @@ -158,7 +154,7 @@ impl Program for Map { (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), diff --git a/src/mask.rs b/src/mask.rs index 2948952..99ad8f1 100644 --- a/src/mask.rs +++ b/src/mask.rs @@ -28,7 +28,23 @@ pub enum State { #[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)] @@ -38,31 +54,34 @@ pub struct Mask { } 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 { - 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 } } @@ -92,13 +111,15 @@ impl Program for Mask { (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); }) }); })] @@ -116,7 +137,7 @@ impl Program for Mask { 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)) -- 2.45.2