mod polygon_draw; use polygon_draw::Polygon; mod music; use music::Music; mod utils; use utils::str_to_sec; use std::fs; use iced::Element; use iced::{ Font, widget::{ TextInput, column, text, text_input::{Icon, Side}, }, }; use iced::{ Length, Task, Theme, widget::{Column, button, canvas, container, pick_list, row, scrollable, slider}, }; use regex::Regex; use std::f32::consts::PI; use std::time::Instant; use kira::{ AudioManager, AudioManagerSettings, DefaultBackend, sound::static_sound::StaticSoundData, }; use crate::utils::delta_to_string; fn main() -> iced::Result { iced::application("My App", MyApp::update, MyApp::view) .theme(|_| Theme::Dark) .subscription(MyApp::subscription) .run_with(MyApp::new) } #[derive(Debug, Clone)] enum Message { ButtonPressedIncrement, ButtonPressedDecrement, Tick, AddPolygon(String), ChangeTeta(usize, f32), Remove(usize), ChangeColor(usize, String), ChangeSound(usize, String), ToggleSavePanel, Save, Load, FileNameChanged(String), TogglePaused, SetMusicLength, LengthChange(String), ChangeDelta(f32), AddPoint, } struct MyApp { music: Music, time_last_frame: Instant, show_save_panel: bool, paused: bool, audio_manager: AudioManager, all_sounds: Vec, all_saves: Vec, current_delta: f32, str_music_length: String, } impl MyApp { fn new() -> (Self, Task) { let manager = AudioManager::::new(AudioManagerSettings::default()) .expect("Error to load AudioManager"); ( Self { time_last_frame: Instant::now(), audio_manager: manager, show_save_panel: false, paused: true, all_sounds: load_path_sounds(), all_saves: load_path_saves(), music: Music::default(), current_delta: 0.0, str_music_length: "01:00:00".to_string(), }, Task::none(), ) } fn update(&mut self, message: Message) { match message { Message::ButtonPressedIncrement => self.music.nb_sec_for_rev += 0.5, Message::ButtonPressedDecrement => { if self.music.nb_sec_for_rev > 0.5 { self.music.nb_sec_for_rev -= 0.5; } } Message::AddPolygon(s) => { self.music.add_polygon(self.current_delta, s); } Message::Tick => { if self.current_delta >= self.music.length { self.paused = true } if !self.paused { self.music.current_delta = self.current_delta; let time_btw = Instant::now().duration_since(self.time_last_frame); self.current_delta += time_btw.as_millis() as f32 / 1000.0; self.music .apply_tick(self.current_delta, time_btw, &mut self.audio_manager); self.time_last_frame = Instant::now(); } } Message::Remove(i) => { self.music.remove_polygon(self.current_delta, i - 1); } Message::ChangeTeta(i, teta) => { self.music.set_teta(self.current_delta, i, teta); } Message::ChangeColor(i, s) => { self.music.set_color(self.current_delta, i, s); } Message::ChangeSound(i, s) => { let sound = StaticSoundData::from_file(format!("./assets/{s}")) .expect("Fail to load audio"); self.music.set_sound(self.current_delta, i, sound, s); } Message::Save => { let json = serde_json::to_string_pretty(&self.music).unwrap(); fs::write(format!("./saves/{0}.pmx", &self.music.file_name), json).unwrap(); self.all_saves = load_path_saves(); } Message::Load => { let json = fs::read_to_string(format!("./saves/{0}.pmx", &self.music.file_name)); match json { Ok(j) => { let decoded: Music = serde_json::from_str(&j).unwrap(); self.music = decoded; self.music.update_frame(); } Err(e) => { eprintln!("Error, no saves with this name to load, {e} "); } } } Message::ToggleSavePanel => self.show_save_panel = !self.show_save_panel, Message::FileNameChanged(s) => self.music.file_name = s, Message::LengthChange(s) => self.str_music_length = s, Message::TogglePaused => { self.paused = !self.paused; if !self.paused { self.time_last_frame = Instant::now(); } } Message::SetMusicLength => { if self.is_length_valid() { self.music.length = str_to_sec(&self.str_music_length) } } Message::ChangeDelta(f) => { self.current_delta = f; self.music.fix_teta(self.current_delta); // update the red dot on canvas if self.paused { self.update(Message::TogglePaused); self.update(Message::Tick); self.update(Message::TogglePaused); } } Message::AddPoint => { self.music.add_point(self.current_delta); } } } fn view(&self) -> iced::Element { let txt_nb_rev = if self.music.nb_sec_for_rev % 1. != 0.0 { format!("{} sec/revolution", self.music.nb_sec_for_rev) } else { format!("{}.0 sec/revolution", self.music.nb_sec_for_rev) }; let mut i = 0; let entries = self.all_sounds.clone(); //Create all polygon options let polygon_rows: Vec> = self .music .current_frame(self.current_delta) .polygons .iter() .map(|polygon| { let current_index = i; i += 1; column![ row![ text(&polygon.name), button("Remove").on_press(Message::Remove(i)), pick_list( ["Black", "Blue", "Green", "Pink", "Yellow", "Cyan"] .map(|s| s.to_string()) .to_vec(), Some(&polygon.color_name), move |s| { Message::ChangeColor(current_index, s) } ), pick_list(entries.clone(), Some(&polygon.sound_name), move |s| { Message::ChangeSound(current_index, s) }), ] .spacing(20), slider(0.0..=2.0 * PI, polygon.global_teta, move |f| { Message::ChangeTeta(current_index, f) }) .step(PI / 84f32), // 84 | 4 for do PI / 4 ] .spacing(10) .into() }) .collect(); let ngon_options: Vec = (5..=42).map(|sides| format!("Ngon{sides}")).collect(); let all_options: Vec = [ "Segment", "Triangle", "Square", "Nr6In30", "Nr7In30", "Nr8In30", "Nr9In30", "Nr8In42", "Nr9In42", "Nr10aIn42", "Nr10bIn42", ] .iter() .map(|s| s.to_string()) .chain(ngon_options) .collect(); let polygon_column = scrollable(Column::with_children(polygon_rows).spacing(20)); let mut save_panel: Vec> = vec![ button("Toggle Save Panel") .on_press(Message::ToggleSavePanel) .into(), ]; if self.show_save_panel { save_panel.push( TextInput::new("Name File", &self.music.file_name) .on_input(|new_value| Message::FileNameChanged(new_value)) .into(), ); save_panel.push(button("Save").on_press(Message::Save).into()); save_panel.push( pick_list( self.all_saves.clone(), Some(&self.music.file_name), move |s| Message::FileNameChanged(s), ) .into(), ); save_panel.push(button("Load").on_press(Message::Load).into()); } column![ text(&self.music.file_name).size(32.0), row(save_panel).spacing(20), row![ text("Music Length").size(20), TextInput::new("MM:SS:CS", &self.str_music_length) .on_input(|new_value| Message::LengthChange(new_value)) .icon(Icon { font: Font::DEFAULT, code_point: if self.is_length_valid() { '\u{2705}' } else { '\u{274C}' }, size: None, spacing: 10., side: Side::Left, }), button("Valid").on_press(Message::SetMusicLength), text(txt_nb_rev).size(20), button("Increment").on_press(Message::ButtonPressedIncrement), button("Decrement").on_press(Message::ButtonPressedDecrement), ] .spacing(20), row![ container( canvas(self.music.current_frame(self.current_delta)) .height(Length::FillPortion(1)) .width(Length::FillPortion(1)) ), column![ text("Polygon options"), pick_list(all_options, Some("Choose polygon".to_string()), |s| { Message::AddPolygon(s) }), polygon_column, ] .spacing(10) .height(Length::FillPortion(1)) .width(Length::FillPortion(2)), ] .height(Length::FillPortion(2)) .spacing(20), column![ row![ button("Toggle Play").on_press(Message::TogglePaused), text(format!( "{}/{}", delta_to_string(self.current_delta), delta_to_string(self.music.length) )) .size(20.0), button("Add Point").on_press(Message::AddPoint) ] .spacing(20), column![ slider(0.0..=self.music.length, self.current_delta, move |f| { Message::ChangeDelta(f) }) .step(&self.music.length / 10_000.), canvas(&self.music) .height(Length::FillPortion(1)) .width(Length::FillPortion(1)) ] .spacing(0), ] .height(Length::FillPortion(1)) .width(Length::FillPortion(1)), ] .spacing(25) .padding(25) .into() } fn is_length_valid(&self) -> bool { let re = Regex::new(r"^\d{2}:\d{2}:\d{2}$").unwrap(); re.is_match(self.str_music_length.as_str()) } fn subscription(&self) -> iced::Subscription { iced::Subscription::batch([ //window::events().map(|(_id, event)| Message::WindowEvent(event)), iced::time::every(std::time::Duration::from_millis(16)).map(|_| Message::Tick), ]) } } fn load_path_sounds() -> Vec { let mut entries: Vec = fs::read_dir("./assets") .unwrap() .filter_map(|res| res.ok()) .map(|e| e.path().file_name().unwrap().to_str().unwrap().to_string()) .collect(); entries.sort(); entries } fn load_path_saves() -> Vec { fs::create_dir_all("./saves").expect("fail to creat 'saves' !"); fs::read_dir("./saves") .unwrap() .filter_map(|res| res.ok()) .map(|e| { e.path() .file_name() .unwrap() .to_str() .unwrap() .trim_end_matches(".pmx") .to_string() }) .collect() }