mod polygon_draw; use polygon_draw::Polygon; mod music; use music::Music; mod message; use message::Message; mod utils; use utils::{is_delta_format_valid, str_to_sec}; use std::fs; use iced::widget::{TextInput, column, text}; use iced::{Application, Element, Font, font}; use iced::{ Color, Length, Task, Theme, widget::{Column, button, canvas, container, pick_list, row, scrollable, slider}, }; use std::f32::consts::PI; use std::time::Instant; use kira::{ AudioManager, AudioManagerSettings, DefaultBackend, sound::static_sound::StaticSoundData, }; const FONT_BYTES: &[u8] = include_bytes!("../fonts/EnvyCodeRNerdFontMono-Regular.ttf"); const FONT: Font = Font::with_name("EnvyCodeR Nerd Font Mono"); use crate::utils::delta_to_string; fn main() -> iced::Result { let polytheme: Theme = { let back = Color::from_rgb8(39, 63, 79); let text = Color::from_rgb8(0xD3, 0xD4, 0xD9); let prim = Color::from_rgb8(230, 82, 31); let succ = Color::from_rgb8(0xFF, 0xF9, 0xFB); let dan = Color::from_rgb8(0xBB, 0x0A, 0x21); Theme::custom( String::from("PolyTheme"), iced::theme::Palette { background: back, text: text, primary: prim, success: succ, danger: dan, }, ) }; iced::application("My App", MyApp::update, MyApp::view) .theme(move |_| polytheme.clone()) .font(FONT_BYTES) .default_font(FONT) .subscription(MyApp::subscription) .run_with(MyApp::new) } 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, str_time: String, } impl MyApp { fn new() -> (Self, Task) { let manager = AudioManager::::new(AudioManagerSettings::default()) .expect("Error to load AudioManager"); //let font_bytes = include_bytes!("../fonts/"); ( 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(), str_time: "00: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(); self.str_time = delta_to_string(self.current_delta); } } 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) => { if is_delta_format_valid(&s) { let sec = str_to_sec(&s); if sec > 0. { self.music.length = sec; } } self.str_music_length = s; } Message::TogglePaused => { self.paused = !self.paused; if !self.paused { self.time_last_frame = Instant::now(); } } Message::ChangeDelta(f) => { self.current_delta = f; self.music.fix_teta(self.current_delta); // update the red dot on canvas self.update_canvas_if_paused(); } Message::ChangeDeltaString(s) => { if is_delta_format_valid(&s) { self.update(Message::ChangeDelta(str_to_sec(&s))); } self.str_time = s; } Message::AddPoint => { self.music.add_point(self.current_delta); } Message::RemovePoint => { self.music.remove_point(self.current_delta); } Message::ClickedOnTimeLine(f) => { self.update(Message::ChangeDelta(f)); } Message::SlidePointLeft => { self.music.slide_to_left(self.current_delta); self.music.fix_teta(self.current_delta); self.update_canvas_if_paused(); } Message::SlidePointRight => { self.music.slide_to_right(self.current_delta); self.music.fix_teta(self.current_delta); self.update_canvas_if_paused(); } } } 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).font(FONT), 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("Polymusic").size(32.0), row(save_panel).spacing(20), row![ 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(text(if self.paused { "󰐊" } else { "󰏤" }).size(28)) .on_press(Message::TogglePaused), TextInput::new("MM:SS:CS", &self.str_time) .on_input(|new_value| Message::ChangeDeltaString(new_value)) .size(28), text("/").size(30), TextInput::new("MM:SS:CS", &self.str_music_length) .on_input(|new_value| Message::LengthChange(new_value)) .size(28), button(text("").size(28)).on_press(Message::AddPoint), button(text("").size(28)).on_press(Message::RemovePoint), button(text("").size(28)).on_press(Message::SlidePointLeft), button(text("").size(28)).on_press(Message::SlidePointRight), ] .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), ] .spacing(20) .height(Length::FillPortion(1)) .width(Length::FillPortion(1)), ] .spacing(25) .padding(25) .into() } 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 update_canvas_if_paused(&mut self) { if self.paused { self.update(Message::TogglePaused); self.update(Message::Tick); self.update(Message::TogglePaused); } } } 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() }