This commit is contained in:
2025-10-04 00:43:45 -05:00
parent 9eec30779a
commit 87d842c2ad
5 changed files with 151 additions and 68 deletions
+27
View File
@@ -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<T> Showable<T> 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<T: AsRef<str>> From<T> for Dialog {
fn from(value: T) -> Self {
Self(value.as_ref().to_string(), true)
}
}
+44 -41
View File
@@ -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<f32> 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)
});
});
}
}
+67 -15
View File
@@ -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<T> {
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::<PersistedTenori>(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<P: AsRef<Path>>(&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::<PersistedTenori>(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<f32> for Tenori {
fn show(&mut self, ctx: &Context, cursor: &f32) {
self.menu(ctx);
self.display_grids(ctx, cursor);
self.display_dialogs(ctx);
}
}
+5 -6
View File
@@ -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)
+8 -6
View File
@@ -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<String>
/// Error dialogs we're currently showing
pub dialogs: Vec<Dialog>,
/// If present, we can save to this file without asking the
/// user to select a file first.
pub default_filename: Option<String>
}
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