wip2
This commit is contained in:
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user