Feat: demo songs
This commit is contained in:
@@ -31,6 +31,8 @@ impl App {
|
||||
layout: self.audio.config.layout,
|
||||
hue_rotation: self.ui.hue_rotation,
|
||||
onboarding_dismissed: self.ui.onboarding_dismissed.clone(),
|
||||
load_demo_on_startup: self.ui.load_demo_on_startup,
|
||||
demo_index: self.ui.demo_index,
|
||||
font: self.ui.font.clone(),
|
||||
zoom_factor: self.ui.zoom_factor,
|
||||
},
|
||||
@@ -91,32 +93,7 @@ impl App {
|
||||
pub fn load(&mut self, path: PathBuf, link: &LinkState) {
|
||||
match model::load(&path) {
|
||||
Ok(project) => {
|
||||
let tempo = project.tempo;
|
||||
let playing = project.playing_patterns.clone();
|
||||
|
||||
self.project_state.project = project;
|
||||
self.editor_ctx.step = 0;
|
||||
self.load_step_to_editor();
|
||||
self.compile_all_steps(link);
|
||||
self.mark_all_patterns_dirty();
|
||||
link.set_tempo(tempo);
|
||||
|
||||
self.playback.clear_queues();
|
||||
self.undo.clear();
|
||||
self.variables.store(Arc::new(HashMap::new()));
|
||||
self.dict.lock().clear();
|
||||
|
||||
self.evaluate_prelude(link);
|
||||
|
||||
for (bank, pattern) in playing {
|
||||
self.playback.queued_changes.push(StagedChange {
|
||||
change: PatternChange::Start { bank, pattern },
|
||||
quantization: crate::model::LaunchQuantization::Immediate,
|
||||
sync_mode: crate::model::SyncMode::PhaseLock,
|
||||
});
|
||||
}
|
||||
|
||||
self.ui.set_status(format!("Loaded: {}", path.display()));
|
||||
self.apply_project(project, format!("Loaded: {}", path.display()), link);
|
||||
self.project_state.file_path = Some(path);
|
||||
}
|
||||
Err(e) => {
|
||||
@@ -124,4 +101,33 @@ impl App {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn apply_project(&mut self, project: model::Project, label: String, link: &LinkState) {
|
||||
let tempo = project.tempo;
|
||||
let playing = project.playing_patterns.clone();
|
||||
|
||||
self.project_state.project = project;
|
||||
self.editor_ctx.step = 0;
|
||||
self.load_step_to_editor();
|
||||
self.compile_all_steps(link);
|
||||
self.mark_all_patterns_dirty();
|
||||
link.set_tempo(tempo);
|
||||
|
||||
self.playback.clear_queues();
|
||||
self.undo.clear();
|
||||
self.variables.store(Arc::new(HashMap::new()));
|
||||
self.dict.lock().clear();
|
||||
|
||||
self.evaluate_prelude(link);
|
||||
|
||||
for (bank, pattern) in playing {
|
||||
self.playback.queued_changes.push(StagedChange {
|
||||
change: PatternChange::Start { bank, pattern },
|
||||
quantization: crate::model::LaunchQuantization::Immediate,
|
||||
sync_mode: crate::model::SyncMode::PhaseLock,
|
||||
});
|
||||
}
|
||||
|
||||
self.ui.set_status(label);
|
||||
}
|
||||
}
|
||||
|
||||
40
src/init.rs
40
src/init.rs
@@ -65,14 +65,38 @@ pub fn init(args: InitArgs) -> Init {
|
||||
|
||||
let mut app = App::new();
|
||||
|
||||
app.playback.queued_changes.push(StagedChange {
|
||||
change: PatternChange::Start {
|
||||
bank: 0,
|
||||
pattern: 0,
|
||||
},
|
||||
quantization: model::LaunchQuantization::Immediate,
|
||||
sync_mode: model::SyncMode::PhaseLock,
|
||||
});
|
||||
app.ui.load_demo_on_startup = settings.display.load_demo_on_startup;
|
||||
app.ui.demo_index = settings.display.demo_index;
|
||||
|
||||
if app.ui.load_demo_on_startup && !model::demos::DEMOS.is_empty() {
|
||||
let count = model::demos::DEMOS.len();
|
||||
let index = settings.display.demo_index % count;
|
||||
let demo = &model::demos::DEMOS[index];
|
||||
if let Ok(project) = model::load_str(demo.json) {
|
||||
let playing = project.playing_patterns.clone();
|
||||
let tempo = project.tempo;
|
||||
app.project_state.project = project;
|
||||
link.set_tempo(tempo);
|
||||
for (bank, pattern) in playing {
|
||||
app.playback.queued_changes.push(StagedChange {
|
||||
change: PatternChange::Start { bank, pattern },
|
||||
quantization: model::LaunchQuantization::Immediate,
|
||||
sync_mode: model::SyncMode::PhaseLock,
|
||||
});
|
||||
}
|
||||
app.ui.set_status(format!("Demo: {}", demo.name));
|
||||
}
|
||||
app.ui.demo_index = (index + 1) % count;
|
||||
} else {
|
||||
app.playback.queued_changes.push(StagedChange {
|
||||
change: PatternChange::Start {
|
||||
bank: 0,
|
||||
pattern: 0,
|
||||
},
|
||||
quantization: model::LaunchQuantization::Immediate,
|
||||
sync_mode: model::SyncMode::PhaseLock,
|
||||
});
|
||||
}
|
||||
|
||||
app.audio.config.output_device = args.output.or(settings.audio.output_device.clone());
|
||||
app.audio.config.input_device = args.input.or(settings.audio.input_device.clone());
|
||||
|
||||
@@ -141,6 +141,9 @@ pub(crate) fn cycle_option_value(ctx: &mut InputContext, right: bool) {
|
||||
OptionsFocus::ResetOnboarding => {
|
||||
ctx.dispatch(AppCommand::ResetOnboarding);
|
||||
}
|
||||
OptionsFocus::LoadDemoOnStartup => {
|
||||
ctx.app.ui.load_demo_on_startup = !ctx.app.ui.load_demo_on_startup;
|
||||
}
|
||||
OptionsFocus::MidiInput0
|
||||
| OptionsFocus::MidiInput1
|
||||
| OptionsFocus::MidiInput2
|
||||
|
||||
17
src/model/demos.rs
Normal file
17
src/model/demos.rs
Normal file
@@ -0,0 +1,17 @@
|
||||
pub struct DemoEntry {
|
||||
pub name: &'static str,
|
||||
pub json: &'static str,
|
||||
}
|
||||
|
||||
pub const DEMOS: &[DemoEntry] = &[
|
||||
DemoEntry { name: "Demo 01", json: include_str!("../../demos/01.cagire") },
|
||||
DemoEntry { name: "Demo 02", json: include_str!("../../demos/02.cagire") },
|
||||
DemoEntry { name: "Demo 03", json: include_str!("../../demos/03.cagire") },
|
||||
DemoEntry { name: "Demo 04", json: include_str!("../../demos/04.cagire") },
|
||||
DemoEntry { name: "Demo 05", json: include_str!("../../demos/05.cagire") },
|
||||
DemoEntry { name: "Demo 06", json: include_str!("../../demos/06.cagire") },
|
||||
DemoEntry { name: "Demo 07", json: include_str!("../../demos/07.cagire") },
|
||||
DemoEntry { name: "Demo 08", json: include_str!("../../demos/08.cagire") },
|
||||
DemoEntry { name: "Demo 09", json: include_str!("../../demos/09.cagire") },
|
||||
DemoEntry { name: "Demo 10", json: include_str!("../../demos/10.cagire") },
|
||||
];
|
||||
@@ -1,4 +1,5 @@
|
||||
pub mod categories;
|
||||
pub mod demos;
|
||||
pub mod docs;
|
||||
pub mod onboarding;
|
||||
mod script;
|
||||
@@ -8,8 +9,8 @@ pub use cagire_forth::{
|
||||
Variables, Word, WordCompile, WORDS,
|
||||
};
|
||||
pub use cagire_project::{
|
||||
load, save, Bank, FollowUp, LaunchQuantization, Pattern, PatternSpeed, Project, SyncMode,
|
||||
MAX_BANKS, MAX_PATTERNS,
|
||||
load, load_str, save, Bank, FollowUp, LaunchQuantization, Pattern, PatternSpeed, Project,
|
||||
SyncMode, MAX_BANKS, MAX_PATTERNS,
|
||||
};
|
||||
pub use script::ScriptEngine;
|
||||
|
||||
|
||||
@@ -58,6 +58,10 @@ pub struct DisplaySettings {
|
||||
pub hue_rotation: f32,
|
||||
#[serde(default)]
|
||||
pub onboarding_dismissed: Vec<String>,
|
||||
#[serde(default = "default_true")]
|
||||
pub load_demo_on_startup: bool,
|
||||
#[serde(default)]
|
||||
pub demo_index: usize,
|
||||
}
|
||||
|
||||
fn default_font() -> String {
|
||||
@@ -105,6 +109,8 @@ impl Default for DisplaySettings {
|
||||
layout: MainLayout::default(),
|
||||
hue_rotation: 0.0,
|
||||
onboarding_dismissed: Vec::new(),
|
||||
load_demo_on_startup: true,
|
||||
demo_index: 0,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -27,6 +27,7 @@ pub enum OptionsFocus {
|
||||
MidiInput2,
|
||||
MidiInput3,
|
||||
ResetOnboarding,
|
||||
LoadDemoOnStartup,
|
||||
}
|
||||
|
||||
impl CyclicEnum for OptionsFocus {
|
||||
@@ -55,6 +56,7 @@ impl CyclicEnum for OptionsFocus {
|
||||
Self::MidiInput2,
|
||||
Self::MidiInput3,
|
||||
Self::ResetOnboarding,
|
||||
Self::LoadDemoOnStartup,
|
||||
];
|
||||
}
|
||||
|
||||
@@ -77,6 +79,7 @@ const STANDALONE_ONLY: &[OptionsFocus] = &[
|
||||
OptionsFocus::MidiInput2,
|
||||
OptionsFocus::MidiInput3,
|
||||
OptionsFocus::ResetOnboarding,
|
||||
OptionsFocus::LoadDemoOnStartup,
|
||||
];
|
||||
|
||||
/// Section layout: header line, divider line, then option lines.
|
||||
@@ -113,6 +116,7 @@ const FULL_LAYOUT: &[(OptionsFocus, usize)] = &[
|
||||
(OptionsFocus::MidiInput3, 39),
|
||||
// blank=40, ONBOARDING header=41, divider=42
|
||||
(OptionsFocus::ResetOnboarding, 43),
|
||||
(OptionsFocus::LoadDemoOnStartup, 44),
|
||||
];
|
||||
|
||||
impl OptionsFocus {
|
||||
|
||||
@@ -79,6 +79,8 @@ pub struct UiState {
|
||||
pub zoom_factor: f32,
|
||||
pub window_width: u32,
|
||||
pub window_height: u32,
|
||||
pub load_demo_on_startup: bool,
|
||||
pub demo_index: usize,
|
||||
}
|
||||
|
||||
impl Default for UiState {
|
||||
@@ -127,6 +129,8 @@ impl Default for UiState {
|
||||
zoom_factor: 1.5,
|
||||
window_width: 1200,
|
||||
window_height: 800,
|
||||
load_demo_on_startup: true,
|
||||
demo_index: 0,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -249,6 +249,12 @@ pub fn render(frame: &mut Frame, app: &App, link: &LinkState, area: Rect) {
|
||||
focus == OptionsFocus::ResetOnboarding,
|
||||
&theme,
|
||||
),
|
||||
render_option_line(
|
||||
"Demo on startup",
|
||||
if app.ui.load_demo_on_startup { "On" } else { "Off" },
|
||||
focus == OptionsFocus::LoadDemoOnStartup,
|
||||
&theme,
|
||||
),
|
||||
]);
|
||||
}
|
||||
|
||||
@@ -358,6 +364,7 @@ fn option_description(focus: OptionsFocus) -> Option<&'static str> {
|
||||
OptionsFocus::MidiInput2 => Some("MIDI input device for channel group 3"),
|
||||
OptionsFocus::MidiInput3 => Some("MIDI input device for channel group 4"),
|
||||
OptionsFocus::ResetOnboarding => Some("Re-enable all dismissed guide popups"),
|
||||
OptionsFocus::LoadDemoOnStartup => Some("Load a rotating demo song on fresh startup"),
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -559,6 +559,9 @@ fn render_mini_tile_grid(
|
||||
let row_gap: u16 = 1;
|
||||
let max_tile_height: u16 = 4;
|
||||
|
||||
let max_rows = (area.height as usize + row_gap as usize) / (1 + row_gap as usize);
|
||||
let num_rows = num_rows.min(max_rows.max(1));
|
||||
|
||||
let available_for_rows =
|
||||
area.height.saturating_sub((num_rows.saturating_sub(1) as u16) * row_gap);
|
||||
let tile_height = (available_for_rows / num_rows as u16).min(max_tile_height).max(1);
|
||||
|
||||
Reference in New Issue
Block a user