diff --git a/src/dialog.rs b/src/dialog.rs new file mode 100644 index 0000000..d256cb7 --- /dev/null +++ b/src/dialog.rs @@ -0,0 +1,27 @@ +use eframe::egui::{Context, Window}; +use crate::gui::Showable; + +/// A struct to represent a simple messagebox, like for an error or something. +/// Contains a string (the message) and a bool which is cleared when the window is +/// closed. +pub struct Dialog(pub String, pub bool); + +impl Showable for Dialog { + fn show(&mut self, ctx: &Context, _state: &T) { + let win = Window::new("Hey!").resizable(false).scroll([false, false]); + let mut open = true; + win.open(&mut open).show(ctx, |ui| { + ui.label(&self.0); + if ui.button("Okay").clicked() { + self.1 = false + } + }); + if !open { self.1 = false } + } +} + +impl> From for Dialog { + fn from(value: T) -> Self { + Self(value.as_ref().to_string(), true) + } +} \ No newline at end of file diff --git a/src/grid.rs b/src/grid.rs index 00c9180..b7ca1fe 100644 --- a/src/grid.rs +++ b/src/grid.rs @@ -1,7 +1,8 @@ use std::ops::RangeInclusive; use eframe::egui; -use eframe::egui::{Color32, Context, Id, PointerButton, Pos2, Rangef, RichText, Sense, Ui, Vec2, Widget}; +use eframe::egui::{Color32, Context, PointerButton, Pos2, Rangef, Sense, Ui, Vec2}; use serde::{Deserialize, Serialize}; +use crate::gui::Showable; use crate::noise::NoteType; use crate::scale::Scale; use crate::tenori::LOOP_LENGTH; @@ -40,46 +41,6 @@ impl Grid { } } - pub fn show(&mut self, ctx: &Context, cursor: f32) { - let win = egui::Window::new(&self.id).resizable(false).scroll([false, false]); - win.show(ctx, |ui| { - egui::MenuBar::new().ui(ui, |ui| { - if ui.button("Clear").clicked() { - self.notes = vec![false; (LOOP_LENGTH * LOOP_LENGTH) as usize] - } - - ui.menu_button("Scale...", |ui| { - if ui.button(Scale::CMajor.label_text(self.scale)).clicked() { - self.scale = Scale::CMajor - } - if ui.button(Scale::CMinor.label_text(self.scale)).clicked() { - self.scale = Scale::CMinor - } - if ui.button(Scale::Chromatic.label_text(self.scale)).clicked() { - self.scale = Scale::Chromatic - } - if ui.button(Scale::Pentatonic.label_text(self.scale)).clicked() { - self.scale = Scale::Pentatonic - } - - }) - }); - - egui::MenuBar::new().ui(ui, |ui| { - ui.label("Volume"); - ui.add(egui::Slider::new(&mut self.volume, RangeInclusive::new(0.0, 2.0)).show_value(false)); - - ui.label("Length"); - ui.add(egui::Slider::new(&mut self.length, RangeInclusive::new(0, 2000)).show_value(false)); - - }); - - egui::Frame::new().inner_margin(3).show(ui, |ui| { - self.draw_grid(ui, cursor) - }); - }); - } - fn draw_grid(&mut self, ui: &mut Ui, cursor: f32) { let dim = 20.0 * LOOP_LENGTH as f32; let (rect, response) = ui.allocate_exact_size(Vec2::new(dim, dim), Sense::click_and_drag()); @@ -127,3 +88,45 @@ impl Grid { notes } } + +impl Showable for Grid { + fn show(&mut self, ctx: &Context, cursor: &f32) { + let win = egui::Window::new(&self.id).resizable(false).scroll([false, false]); + win.show(ctx, |ui| { + egui::MenuBar::new().ui(ui, |ui| { + if ui.button("Clear").clicked() { + self.notes = vec![false; (LOOP_LENGTH * LOOP_LENGTH) as usize] + } + + ui.menu_button("Scale...", |ui| { + if ui.button(Scale::CMajor.label_text(self.scale)).clicked() { + self.scale = Scale::CMajor + } + if ui.button(Scale::CMinor.label_text(self.scale)).clicked() { + self.scale = Scale::CMinor + } + if ui.button(Scale::Chromatic.label_text(self.scale)).clicked() { + self.scale = Scale::Chromatic + } + if ui.button(Scale::Pentatonic.label_text(self.scale)).clicked() { + self.scale = Scale::Pentatonic + } + + }) + }); + + egui::MenuBar::new().ui(ui, |ui| { + ui.label("Volume"); + ui.add(egui::Slider::new(&mut self.volume, RangeInclusive::new(0.0, 2.0)).show_value(false)); + + ui.label("Length"); + ui.add(egui::Slider::new(&mut self.length, RangeInclusive::new(0, 2000)).show_value(false)); + + }); + + egui::Frame::new().inner_margin(3).show(ui, |ui| { + self.draw_grid(ui, *cursor) + }); + }); + } +} \ No newline at end of file diff --git a/src/gui.rs b/src/gui.rs index aff8f90..ed6a41e 100644 --- a/src/gui.rs +++ b/src/gui.rs @@ -1,33 +1,35 @@ use std::fs; use std::ops::RangeInclusive; +use std::path::Path; use eframe::egui; use eframe::egui::{Context, TopBottomPanel}; use crate::noise::NoteType; use crate::saveload::PersistedTenori; use crate::Tenori; +/// A trait for things that can be shown in a gui, given a Context. +pub trait Showable { + fn show(&mut self, ctx: &Context, state: &T); +} + impl Tenori { - pub fn menu(&mut self, ctx: &Context) { + fn menu(&mut self, ctx: &Context) { TopBottomPanel::top("menu_panel").show(ctx, |ui| { egui::MenuBar::new().ui(ui, |ui| { ui.menu_button("File", |ui| { - if ui.button("Save").clicked() && - let Ok(serialized) = toml::to_string(&PersistedTenori::from(&*self)) && - let Some(path) = rfd::FileDialog::new().add_filter("Tenori files", &["tenori"]).set_file_name("song.tenori").save_file() { - match fs::write(path, serialized) { - Ok(_) => {} - Err(e) => { dbg!(e); } + if ui.button("Load").clicked() && let Err(s) = self.load_from_file() { + self.dialogs.push(s.into()) + } + + if ui.add_enabled(self.default_filename.is_some(), egui::Button::new("Save")).clicked() { + let path = self.default_filename.as_ref().unwrap_or_else(|| unreachable!()); + if let Err(s) = self.save_to_file(path) { + self.dialogs.push(s.into()) } } - if ui.button("Load").clicked() && - let Some(path) = rfd::FileDialog::new().add_filter("Tenori files", &["tenori"]).pick_file() && - let Ok(serialized) = fs::read_to_string(path) && - let Ok(persisted) = toml::from_str::(serialized.as_str()) { - self.grids = persisted.grids; - self.tempo = persisted.tempo; - self.playing = false; - self.timer = 0.0; + if ui.button("Save As...").clicked() && let Err(s) = self.save_as() { + self.dialogs.push(s.into()) } }); @@ -60,4 +62,54 @@ impl Tenori { }) }); } + + pub fn display_dialogs(&mut self, ctx: &Context) { + for d in self.dialogs.iter_mut() { + d.show(ctx, &()); + } + self.dialogs.retain(|d| d.1); + } + + fn save_as(&self) -> Result<(), String> { + if let Some(path) = rfd::FileDialog::new() + .add_filter("Tenori files", &["tenori"]) + .set_file_name("song.tenori").save_file() { + return self.save_to_file(path) + } + Ok(()) + } + + fn save_to_file>(&self, filename: P) -> Result<(), String> { + let serialized = toml::to_string(&PersistedTenori::from(&*self)).map_err(|e| e.to_string())?; + fs::write(filename, serialized).map_err(|e| e.to_string()) + } + + fn load_from_file(&mut self) -> Result<(), String> { + if let Some(path) = rfd::FileDialog::new() + .add_filter("Tenori files", &["tenori"]) + .pick_file() { + let serialized = fs::read_to_string(path).map_err(|e| e.to_string())?; + let persisted = toml::from_str::(serialized.as_str()).map_err(|e| e.to_string())?; + + self.grids = persisted.grids; + self.tempo = persisted.tempo; + self.playing = false; + self.timer = 0.0; + } + Ok(()) + } + + fn display_grids(&mut self, ctx: &Context, cursor: &f32) { + for g in self.grids.iter_mut() { + g.show(ctx, cursor) + } + } +} + +impl Showable for Tenori { + fn show(&mut self, ctx: &Context, cursor: &f32) { + self.menu(ctx); + self.display_grids(ctx, cursor); + self.display_dialogs(ctx); + } } \ No newline at end of file diff --git a/src/main.rs b/src/main.rs index 6eba88e..479d6c2 100644 --- a/src/main.rs +++ b/src/main.rs @@ -4,10 +4,12 @@ mod grid; mod noise; mod scale; mod saveload; +mod dialog; use std::time::Duration; use eframe::{App, Frame}; use eframe::egui::Context; +use crate::gui::Showable; use crate::tenori::Tenori; #[tokio::main] @@ -19,15 +21,12 @@ async fn main() { } impl App for Tenori { - fn update(&mut self, ctx: &Context, frame: &mut Frame) { + fn update(&mut self, ctx: &Context, _frame: &mut Frame) { let play = self.tick(); let cursor = self.ratio(); - self.menu(ctx); - for g in self.grids.iter_mut() { - g.show(ctx, cursor) - } - for d in self.dialogs + self.show(ctx, &cursor); + if play { for note in self.notes_for_beat() { self.play(note) diff --git a/src/tenori.rs b/src/tenori.rs index ff0e138..fe91adc 100644 --- a/src/tenori.rs +++ b/src/tenori.rs @@ -1,6 +1,7 @@ use std::time::{Duration, Instant}; use rodio::OutputStream; use crate::grid::Grid; +use crate::dialog::Dialog; use crate::noise::{Note, NoteType}; pub const LOOP_LENGTH: u32 = 16; @@ -27,8 +28,12 @@ pub struct Tenori { // The audio output stream to which we will play notes output_stream: OutputStream, - // Error dialogs we're currently showing - dialogs: Vec + /// Error dialogs we're currently showing + pub dialogs: Vec, + + /// If present, we can save to this file without asking the + /// user to select a file first. + pub default_filename: Option } impl Default for Tenori { @@ -44,6 +49,7 @@ impl Default for Tenori { grids: vec![], window_counter: 0, dialogs: vec![], + default_filename: None, output_stream } } @@ -73,10 +79,6 @@ impl Tenori { self.beat() != old_beat } - pub fn display_dialogs(&mut self) { - - } - /// Which beat (0..loop_length) we're on pub fn beat(&self) -> u32 { self.timer.floor() as u32