Compare commits
2 Commits
4743c33916
...
5a72e4cef4
| Author | SHA1 | Date | |
|---|---|---|---|
| 5a72e4cef4 | |||
| 0097777449 |
@@ -1573,7 +1573,7 @@ impl Forth {
|
||||
} else {
|
||||
let note = get_int("note").unwrap_or(60).clamp(0, 127) as u8;
|
||||
let velocity =
|
||||
get_int("velocity").unwrap_or(100).clamp(0, 127) as u8;
|
||||
(get_float("velocity").unwrap_or(0.8) * 127.0).clamp(0.0, 127.0) as u8;
|
||||
let dur = get_float("dur").unwrap_or(1.0);
|
||||
let dur_secs = dur * ctx.step_duration();
|
||||
outputs.push(format!(
|
||||
|
||||
@@ -28,14 +28,14 @@ pub(super) const WORDS: &[Word] = &[
|
||||
aliases: &[],
|
||||
category: "Envelope",
|
||||
stack: "(v.. --)",
|
||||
desc: "Set velocity",
|
||||
example: "100 velocity",
|
||||
desc: "Set velocity (0-1)",
|
||||
example: "0.8 velocity",
|
||||
compile: Param,
|
||||
varargs: true,
|
||||
},
|
||||
Word {
|
||||
name: "attack",
|
||||
aliases: &["att"],
|
||||
aliases: &["att", "a"],
|
||||
category: "Envelope",
|
||||
stack: "(v.. --)",
|
||||
desc: "Set attack time",
|
||||
@@ -45,7 +45,7 @@ pub(super) const WORDS: &[Word] = &[
|
||||
},
|
||||
Word {
|
||||
name: "decay",
|
||||
aliases: &["dec"],
|
||||
aliases: &["dec", "d"],
|
||||
category: "Envelope",
|
||||
stack: "(v.. --)",
|
||||
desc: "Set decay time",
|
||||
@@ -55,7 +55,7 @@ pub(super) const WORDS: &[Word] = &[
|
||||
},
|
||||
Word {
|
||||
name: "sustain",
|
||||
aliases: &["sus"],
|
||||
aliases: &["sus", "s"],
|
||||
category: "Envelope",
|
||||
stack: "(v.. --)",
|
||||
desc: "Set sustain level",
|
||||
@@ -65,7 +65,7 @@ pub(super) const WORDS: &[Word] = &[
|
||||
},
|
||||
Word {
|
||||
name: "release",
|
||||
aliases: &["rel"],
|
||||
aliases: &["rel", "r"],
|
||||
category: "Envelope",
|
||||
stack: "(v.. --)",
|
||||
desc: "Set release time",
|
||||
|
||||
@@ -6,7 +6,7 @@ pub(super) const WORDS: &[Word] = &[
|
||||
// Sound
|
||||
Word {
|
||||
name: "sound",
|
||||
aliases: &["s"],
|
||||
aliases: &["snd"],
|
||||
category: "Sound",
|
||||
stack: "(name --)",
|
||||
desc: "Begin sound command",
|
||||
@@ -377,6 +377,16 @@ pub(super) const WORDS: &[Word] = &[
|
||||
compile: Param,
|
||||
varargs: true,
|
||||
},
|
||||
Word {
|
||||
name: "partials",
|
||||
aliases: &[],
|
||||
category: "Oscillator",
|
||||
stack: "(v.. --)",
|
||||
desc: "Set number of active harmonics (add source only)",
|
||||
example: "16 partials",
|
||||
compile: Param,
|
||||
varargs: true,
|
||||
},
|
||||
Word {
|
||||
name: "coarse",
|
||||
aliases: &[],
|
||||
|
||||
@@ -137,10 +137,10 @@ impl<'a> SampleBrowser<'a> {
|
||||
|
||||
let (icon, icon_color) = match entry.kind {
|
||||
TreeLineKind::Root { expanded: true } | TreeLineKind::Folder { expanded: true } => {
|
||||
("\u{25BC} ", colors.browser.folder_icon)
|
||||
("\u{2212} ", colors.browser.folder_icon)
|
||||
}
|
||||
TreeLineKind::Root { expanded: false }
|
||||
| TreeLineKind::Folder { expanded: false } => ("\u{25B6} ", colors.browser.folder_icon),
|
||||
| TreeLineKind::Folder { expanded: false } => ("+ ", colors.browser.folder_icon),
|
||||
TreeLineKind::File => ("\u{266A} ", colors.browser.file_icon),
|
||||
};
|
||||
|
||||
|
||||
@@ -339,12 +339,6 @@ impl App {
|
||||
}
|
||||
AppCommand::ToggleLiveKeysFill => self.live_keys.flip_fill(),
|
||||
|
||||
// Panel
|
||||
AppCommand::ClosePanel => {
|
||||
self.panel.visible = false;
|
||||
self.panel.focus = crate::state::PanelFocus::Main;
|
||||
}
|
||||
|
||||
// Direct navigation (mouse)
|
||||
AppCommand::GoToStep(step) => {
|
||||
let len = self.current_edit_pattern().length;
|
||||
|
||||
@@ -25,8 +25,8 @@ use crate::model::{self, Bank, Dictionary, Pattern, Rng, ScriptEngine, Variables
|
||||
use crate::page::Page;
|
||||
use crate::state::{
|
||||
undo::UndoHistory, AudioSettings, EditorContext, LiveKeyState, Metrics, Modal,
|
||||
OptionsState, PanelState, PatternField, PatternPropsField, PatternsNav, PlaybackState,
|
||||
ProjectState, ScriptEditorState, UiState,
|
||||
OptionsState, PatternField, PatternPropsField, PatternsNav, PlaybackState,
|
||||
ProjectState, SampleBrowserState, ScriptEditorState, UiState,
|
||||
};
|
||||
|
||||
static COMPLETION_CANDIDATES: LazyLock<Arc<[CompletionCandidate]>> = LazyLock::new(|| {
|
||||
@@ -66,7 +66,7 @@ pub struct App {
|
||||
|
||||
pub audio: AudioSettings,
|
||||
pub options: OptionsState,
|
||||
pub panel: PanelState,
|
||||
pub sample_browser: Option<SampleBrowserState>,
|
||||
pub midi: MidiState,
|
||||
pub plugin_mode: bool,
|
||||
}
|
||||
@@ -123,7 +123,7 @@ impl App {
|
||||
AudioSettings::default()
|
||||
},
|
||||
options: OptionsState::default(),
|
||||
panel: PanelState::default(),
|
||||
sample_browser: None,
|
||||
midi: MidiState::new(),
|
||||
plugin_mode,
|
||||
}
|
||||
@@ -213,6 +213,9 @@ impl App {
|
||||
if self.ui.modal != Modal::None {
|
||||
return;
|
||||
}
|
||||
if crate::model::onboarding::for_page(self.page).is_empty() {
|
||||
return;
|
||||
}
|
||||
let name = self.page.name();
|
||||
if self.ui.onboarding_dismissed.iter().any(|d| d == name) {
|
||||
return;
|
||||
|
||||
@@ -232,9 +232,6 @@ pub enum AppCommand {
|
||||
// Live keys
|
||||
ToggleLiveKeysFill,
|
||||
|
||||
// Panel
|
||||
ClosePanel,
|
||||
|
||||
// Direct navigation (mouse)
|
||||
GoToStep(usize),
|
||||
PatternsSelectBank(usize),
|
||||
|
||||
@@ -3,9 +3,10 @@ use std::sync::atomic::Ordering;
|
||||
|
||||
use super::{InputContext, InputResult};
|
||||
use crate::commands::AppCommand;
|
||||
use crate::page::Page;
|
||||
use crate::state::{
|
||||
ConfirmAction, CyclicEnum, EuclideanField, Modal, PanelFocus, PatternField, RenameTarget,
|
||||
SampleBrowserState, SidePanel,
|
||||
ConfirmAction, CyclicEnum, EuclideanField, Modal, PatternField, RenameTarget,
|
||||
SampleBrowserState,
|
||||
};
|
||||
|
||||
pub(super) fn handle_main_page(ctx: &mut InputContext, key: KeyEvent, ctrl: bool) -> InputResult {
|
||||
@@ -13,15 +14,11 @@ pub(super) fn handle_main_page(ctx: &mut InputContext, key: KeyEvent, ctrl: bool
|
||||
|
||||
match key.code {
|
||||
KeyCode::Tab => {
|
||||
if ctx.app.panel.visible {
|
||||
ctx.app.panel.visible = false;
|
||||
ctx.app.panel.focus = PanelFocus::Main;
|
||||
} else {
|
||||
let state = SampleBrowserState::new(&ctx.app.audio.config.sample_paths);
|
||||
ctx.app.panel.side = Some(SidePanel::SampleBrowser(state));
|
||||
ctx.app.panel.visible = true;
|
||||
ctx.app.panel.focus = PanelFocus::Side;
|
||||
if ctx.app.sample_browser.is_none() {
|
||||
ctx.app.sample_browser =
|
||||
Some(SampleBrowserState::new(&ctx.app.audio.config.sample_paths));
|
||||
}
|
||||
ctx.dispatch(AppCommand::GoToPage(Page::SampleExplorer));
|
||||
}
|
||||
KeyCode::Char('q') if !ctx.app.plugin_mode => {
|
||||
ctx.dispatch(AppCommand::OpenModal(Modal::Confirm {
|
||||
|
||||
@@ -6,8 +6,8 @@ mod main_page;
|
||||
mod modal;
|
||||
mod mouse;
|
||||
pub(crate) mod options_page;
|
||||
mod panel;
|
||||
mod patterns_page;
|
||||
mod sample_explorer;
|
||||
mod script_page;
|
||||
|
||||
use arc_swap::ArcSwap;
|
||||
@@ -22,7 +22,7 @@ use crate::app::App;
|
||||
use crate::commands::AppCommand;
|
||||
use crate::engine::{AudioCommand, LinkState, SeqCommand, SequencerSnapshot};
|
||||
use crate::page::Page;
|
||||
use crate::state::{MinimapMode, Modal, PanelFocus};
|
||||
use crate::state::{MinimapMode, Modal};
|
||||
|
||||
pub enum InputResult {
|
||||
Continue,
|
||||
@@ -106,10 +106,6 @@ fn handle_normal_input(ctx: &mut InputContext, key: KeyEvent) -> InputResult {
|
||||
return InputResult::Continue;
|
||||
}
|
||||
|
||||
if ctx.app.panel.visible && ctx.app.panel.focus == PanelFocus::Side {
|
||||
return panel::handle_panel_input(ctx, key);
|
||||
}
|
||||
|
||||
if alt {
|
||||
match key.code {
|
||||
KeyCode::Up => {
|
||||
@@ -200,6 +196,7 @@ fn handle_normal_input(ctx: &mut InputContext, key: KeyEvent) -> InputResult {
|
||||
Page::Help => help_page::handle_help_page(ctx, key),
|
||||
Page::Dict => help_page::handle_dict_page(ctx, key),
|
||||
Page::Script => script_page::handle_script_page(ctx, key),
|
||||
Page::SampleExplorer => sample_explorer::handle_sample_explorer(ctx, key),
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -211,22 +211,16 @@ fn handle_scroll(ctx: &mut InputContext, col: u16, row: u16, term: Rect, up: boo
|
||||
return;
|
||||
}
|
||||
|
||||
// Scroll over side panel area
|
||||
if ctx.app.panel.visible && ctx.app.panel.side.is_some() {
|
||||
let (_main_area, side_area) = panel_split(body);
|
||||
if contains(side_area, col, row) {
|
||||
if let Some(crate::state::SidePanel::SampleBrowser(state)) = &mut ctx.app.panel.side {
|
||||
match ctx.app.page {
|
||||
Page::SampleExplorer => {
|
||||
if let Some(state) = &mut ctx.app.sample_browser {
|
||||
if up {
|
||||
state.move_up();
|
||||
} else {
|
||||
state.move_down();
|
||||
}
|
||||
}
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
match ctx.app.page {
|
||||
Page::Main => {
|
||||
if up {
|
||||
ctx.dispatch(AppCommand::StepUp);
|
||||
@@ -362,6 +356,7 @@ fn handle_footer_click(ctx: &mut InputContext, col: u16, row: u16, footer: Rect)
|
||||
Page::Help => " HELP ",
|
||||
Page::Dict => " DICT ",
|
||||
Page::Script => " SCRIPT ",
|
||||
Page::SampleExplorer => " SAMPLES ",
|
||||
};
|
||||
let badge_end = block_inner.x + badge_text.len() as u16;
|
||||
if col < badge_end {
|
||||
@@ -371,73 +366,20 @@ fn handle_footer_click(ctx: &mut InputContext, col: u16, row: u16, footer: Rect)
|
||||
|
||||
// --- Body ---
|
||||
|
||||
fn panel_split(body: Rect) -> (Rect, Rect) {
|
||||
if body.width >= 120 {
|
||||
let panel_width = body.width * 35 / 100;
|
||||
let [main, side] =
|
||||
Layout::horizontal([Constraint::Fill(1), Constraint::Length(panel_width)])
|
||||
.areas(body);
|
||||
(main, side)
|
||||
} else {
|
||||
let panel_height = body.height * 40 / 100;
|
||||
let [main, side] =
|
||||
Layout::vertical([Constraint::Fill(1), Constraint::Length(panel_height)])
|
||||
.areas(body);
|
||||
(main, side)
|
||||
}
|
||||
}
|
||||
|
||||
fn handle_body_click(ctx: &mut InputContext, col: u16, row: u16, body: Rect, kind: ClickKind) {
|
||||
use crate::state::PanelFocus;
|
||||
|
||||
if ctx.app.panel.visible && ctx.app.panel.side.is_some() {
|
||||
let (main_area, side_area) = panel_split(body);
|
||||
|
||||
if contains(side_area, col, row) {
|
||||
ctx.app.panel.focus = PanelFocus::Side;
|
||||
return;
|
||||
}
|
||||
|
||||
// Click on main area: defocus panel
|
||||
if contains(main_area, col, row) {
|
||||
if kind == ClickKind::Double {
|
||||
ctx.dispatch(AppCommand::ClosePanel);
|
||||
} else {
|
||||
ctx.app.panel.focus = PanelFocus::Main;
|
||||
}
|
||||
}
|
||||
|
||||
// Fall through to page-specific handler with main_area
|
||||
if !contains(main_area, col, row) {
|
||||
return;
|
||||
}
|
||||
|
||||
match ctx.app.page {
|
||||
Page::Main => handle_main_click(ctx, col, row, main_area, kind),
|
||||
Page::Patterns => handle_patterns_click(ctx, col, row, main_area, kind),
|
||||
Page::Help => handle_help_click(ctx, col, row, main_area),
|
||||
Page::Dict => handle_dict_click(ctx, col, row, main_area),
|
||||
Page::Options => handle_options_click(ctx, col, row, main_area),
|
||||
Page::Engine => handle_engine_click(ctx, col, row, main_area, kind),
|
||||
Page::Script => handle_script_click(ctx, col, row, main_area),
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
let page_area = body;
|
||||
|
||||
if !contains(page_area, col, row) {
|
||||
if !contains(body, col, row) {
|
||||
return;
|
||||
}
|
||||
|
||||
match ctx.app.page {
|
||||
Page::Main => handle_main_click(ctx, col, row, page_area, kind),
|
||||
Page::Patterns => handle_patterns_click(ctx, col, row, page_area, kind),
|
||||
Page::Help => handle_help_click(ctx, col, row, page_area),
|
||||
Page::Dict => handle_dict_click(ctx, col, row, page_area),
|
||||
Page::Options => handle_options_click(ctx, col, row, page_area),
|
||||
Page::Engine => handle_engine_click(ctx, col, row, page_area, kind),
|
||||
Page::Script => handle_script_click(ctx, col, row, page_area),
|
||||
Page::Main => handle_main_click(ctx, col, row, body, kind),
|
||||
Page::Patterns => handle_patterns_click(ctx, col, row, body, kind),
|
||||
Page::Help => handle_help_click(ctx, col, row, body),
|
||||
Page::Dict => handle_dict_click(ctx, col, row, body),
|
||||
Page::Options => handle_options_click(ctx, col, row, body),
|
||||
Page::Engine => handle_engine_click(ctx, col, row, body, kind),
|
||||
Page::Script => handle_script_click(ctx, col, row, body),
|
||||
Page::SampleExplorer => {}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -923,20 +865,7 @@ fn handle_script_editor_drag(ctx: &mut InputContext, col: u16, row: u16, term: R
|
||||
if ctx.app.script_editor.mouse_selecting {
|
||||
let padded = padded(term);
|
||||
let (_header, body, _footer) = top_level_layout(padded);
|
||||
let page_area = if ctx.app.panel.visible && ctx.app.panel.side.is_some() {
|
||||
if body.width >= 120 {
|
||||
let panel_width = body.width * 35 / 100;
|
||||
Layout::horizontal([Constraint::Fill(1), Constraint::Length(panel_width)])
|
||||
.split(body)[0]
|
||||
} else {
|
||||
let panel_height = body.height * 40 / 100;
|
||||
Layout::vertical([Constraint::Fill(1), Constraint::Length(panel_height)])
|
||||
.split(body)[0]
|
||||
}
|
||||
} else {
|
||||
body
|
||||
};
|
||||
handle_script_editor_mouse(ctx, col, row, page_area, true);
|
||||
handle_script_editor_mouse(ctx, col, row, body, true);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -3,12 +3,12 @@ use crossterm::event::{KeyCode, KeyEvent, KeyModifiers};
|
||||
use super::{InputContext, InputResult};
|
||||
use crate::commands::AppCommand;
|
||||
use crate::engine::AudioCommand;
|
||||
use crate::state::SidePanel;
|
||||
use crate::page::Page;
|
||||
use cagire_ratatui::TreeLineKind;
|
||||
|
||||
pub(super) fn handle_panel_input(ctx: &mut InputContext, key: KeyEvent) -> InputResult {
|
||||
let state = match &mut ctx.app.panel.side {
|
||||
Some(SidePanel::SampleBrowser(s)) => s,
|
||||
pub(super) fn handle_sample_explorer(ctx: &mut InputContext, key: KeyEvent) -> InputResult {
|
||||
let state = match &mut ctx.app.sample_browser {
|
||||
Some(s) => s,
|
||||
None => return InputResult::Continue,
|
||||
};
|
||||
|
||||
@@ -86,11 +86,11 @@ pub(super) fn handle_panel_input(ctx: &mut InputContext, key: KeyEvent) -> Input
|
||||
if state.has_filter() {
|
||||
state.clear_filter();
|
||||
} else {
|
||||
ctx.dispatch(AppCommand::ClosePanel);
|
||||
ctx.dispatch(AppCommand::GoToPage(Page::Main));
|
||||
}
|
||||
}
|
||||
KeyCode::Tab => {
|
||||
ctx.dispatch(AppCommand::ClosePanel);
|
||||
ctx.dispatch(AppCommand::GoToPage(Page::Main));
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
@@ -4,7 +4,7 @@ pub fn for_page(page: Page) -> &'static [(&'static str, &'static [(&'static str,
|
||||
match page {
|
||||
Page::Main => &[
|
||||
(
|
||||
"The step sequencer grid. Each cell is a Forth script that produces sound when evaluated. During playback, active steps run left-to-right, top-to-bottom. Toggle steps on/off with t to build your pattern. The left panel shows playing patterns, the right side shows VU meters.",
|
||||
"The step sequencer grid. Each cell is a Forth script that produces sound when evaluated. During playback, active steps run left-to-right, top-to-bottom. Toggle steps on/off with t to build your pattern.",
|
||||
&[
|
||||
("Arrows", "navigate grid"),
|
||||
("Space", "play / stop"),
|
||||
@@ -101,5 +101,6 @@ pub fn for_page(page: Page) -> &'static [(&'static str, &'static [(&'static str,
|
||||
("Ctrl+S", "toggle stack preview"),
|
||||
],
|
||||
)],
|
||||
Page::SampleExplorer => &[],
|
||||
}
|
||||
}
|
||||
|
||||
19
src/page.rs
19
src/page.rs
@@ -8,10 +8,11 @@ pub enum Page {
|
||||
Dict,
|
||||
Options,
|
||||
Script,
|
||||
SampleExplorer,
|
||||
}
|
||||
|
||||
impl Page {
|
||||
/// All pages for iteration (grid pages only — Script excluded)
|
||||
/// All pages for iteration (grid pages only — Script and SampleExplorer excluded)
|
||||
pub const ALL: &'static [Page] = &[
|
||||
Page::Main,
|
||||
Page::Patterns,
|
||||
@@ -29,7 +30,7 @@ impl Page {
|
||||
/// col 0 col 1 col 2
|
||||
/// row 0 Dict Patterns Options
|
||||
/// row 1 Help Sequencer Engine
|
||||
/// Script lives outside the grid at (1, 2)
|
||||
/// Script and SampleExplorer live outside the grid at (1, 2)
|
||||
pub const fn grid_pos(self) -> (i8, i8) {
|
||||
match self {
|
||||
Page::Dict => (0, 0),
|
||||
@@ -38,7 +39,7 @@ impl Page {
|
||||
Page::Main => (1, 1),
|
||||
Page::Options => (2, 0),
|
||||
Page::Engine => (2, 1),
|
||||
Page::Script => (1, 2),
|
||||
Page::Script | Page::SampleExplorer => (1, 2),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -57,11 +58,12 @@ impl Page {
|
||||
Page::Dict => "Dict",
|
||||
Page::Options => "Options",
|
||||
Page::Script => "Script",
|
||||
Page::SampleExplorer => "Samples",
|
||||
}
|
||||
}
|
||||
|
||||
pub fn left(&mut self) {
|
||||
if *self == Page::Script {
|
||||
if matches!(*self, Page::Script | Page::SampleExplorer) {
|
||||
*self = Page::Help;
|
||||
return;
|
||||
}
|
||||
@@ -76,7 +78,7 @@ impl Page {
|
||||
}
|
||||
|
||||
pub fn right(&mut self) {
|
||||
if *self == Page::Script {
|
||||
if matches!(*self, Page::Script | Page::SampleExplorer) {
|
||||
*self = Page::Engine;
|
||||
return;
|
||||
}
|
||||
@@ -91,7 +93,7 @@ impl Page {
|
||||
}
|
||||
|
||||
pub fn up(&mut self) {
|
||||
if *self == Page::Script {
|
||||
if matches!(*self, Page::Script | Page::SampleExplorer) {
|
||||
*self = Page::Main;
|
||||
return;
|
||||
}
|
||||
@@ -115,13 +117,12 @@ impl Page {
|
||||
Page::Engine => Some(14), // "Introduction" (Audio Engine)
|
||||
Page::Help => Some(0), // "Welcome"
|
||||
Page::Dict => Some(7), // "About Forth"
|
||||
Page::Options => None,
|
||||
Page::Script => None,
|
||||
Page::Options | Page::Script | Page::SampleExplorer => None,
|
||||
}
|
||||
}
|
||||
|
||||
/// Whether this page appears in the navigation minimap grid.
|
||||
pub const fn visible_in_minimap(self) -> bool {
|
||||
!matches!(self, Page::Script)
|
||||
!matches!(self, Page::Script | Page::SampleExplorer)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -21,7 +21,6 @@ pub mod file_browser;
|
||||
pub mod live_keys;
|
||||
pub mod modal;
|
||||
pub mod options;
|
||||
pub mod panel;
|
||||
pub mod patterns_nav;
|
||||
pub mod playback;
|
||||
pub mod project;
|
||||
@@ -38,7 +37,6 @@ pub use editor::{
|
||||
pub use live_keys::LiveKeyState;
|
||||
pub use modal::{ConfirmAction, Modal, RenameTarget};
|
||||
pub use options::{OptionsFocus, OptionsState};
|
||||
pub use panel::{PanelFocus, PanelState, SidePanel};
|
||||
pub use patterns_nav::{PatternsColumn, PatternsNav};
|
||||
pub use playback::{PlaybackState, StagedChange, StagedMuteChange, StagedPropChange};
|
||||
pub use project::ProjectState;
|
||||
|
||||
@@ -1,28 +0,0 @@
|
||||
use super::sample_browser::SampleBrowserState;
|
||||
|
||||
#[derive(Clone, Copy, PartialEq, Eq, Default)]
|
||||
pub enum PanelFocus {
|
||||
#[default]
|
||||
Main,
|
||||
Side,
|
||||
}
|
||||
|
||||
pub enum SidePanel {
|
||||
SampleBrowser(SampleBrowserState),
|
||||
}
|
||||
|
||||
pub struct PanelState {
|
||||
pub side: Option<SidePanel>,
|
||||
pub focus: PanelFocus,
|
||||
pub visible: bool,
|
||||
}
|
||||
|
||||
impl Default for PanelState {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
side: None,
|
||||
focus: PanelFocus::Main,
|
||||
visible: false,
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -6,6 +6,7 @@ pub mod main_view;
|
||||
pub mod options_view;
|
||||
pub mod patterns_view;
|
||||
mod render;
|
||||
pub mod sample_explorer_view;
|
||||
pub mod script_view;
|
||||
pub mod title_view;
|
||||
|
||||
|
||||
@@ -14,19 +14,18 @@ use crate::engine::{LinkState, SequencerSnapshot};
|
||||
use crate::model::{ExecutionTrace, SourceSpan};
|
||||
use crate::page::Page;
|
||||
use crate::state::{
|
||||
EditorTarget, EuclideanField, FlashKind, Modal, PanelFocus, PatternField, RenameTarget,
|
||||
SidePanel,
|
||||
EditorTarget, EuclideanField, FlashKind, Modal, PatternField, RenameTarget,
|
||||
};
|
||||
use crate::theme;
|
||||
use crate::views::highlight::{self, highlight_line_with_runtime};
|
||||
use crate::widgets::{
|
||||
hint_line, render_props_form, render_search_bar, ConfirmModal, ModalFrame, NavMinimap, NavTile,
|
||||
SampleBrowser, TextInputModal,
|
||||
TextInputModal,
|
||||
};
|
||||
|
||||
use super::{
|
||||
dict_view, engine_view, help_view, main_view, options_view, patterns_view, script_view,
|
||||
title_view,
|
||||
dict_view, engine_view, help_view, main_view, options_view, patterns_view,
|
||||
sample_explorer_view, script_view, title_view,
|
||||
};
|
||||
|
||||
fn clip_span(span: SourceSpan, line_start: usize, line_len: usize) -> Option<SourceSpan> {
|
||||
@@ -164,28 +163,15 @@ pub fn render(
|
||||
render_header(frame, app, link, snapshot, header_area);
|
||||
}
|
||||
|
||||
let (page_area, panel_area) = if app.panel.visible && app.panel.side.is_some() {
|
||||
let panel_width = body_area.width * 35 / 100;
|
||||
let [main, side] =
|
||||
Layout::horizontal([Constraint::Fill(1), Constraint::Length(panel_width)])
|
||||
.areas(body_area);
|
||||
(main, Some(side))
|
||||
} else {
|
||||
(body_area, None)
|
||||
};
|
||||
|
||||
match app.page {
|
||||
Page::Main => main_view::render(frame, app, snapshot, page_area),
|
||||
Page::Patterns => patterns_view::render(frame, app, snapshot, page_area),
|
||||
Page::Engine => engine_view::render(frame, app, link, page_area),
|
||||
Page::Options => options_view::render(frame, app, page_area),
|
||||
Page::Help => help_view::render(frame, app, page_area),
|
||||
Page::Dict => dict_view::render(frame, app, page_area),
|
||||
Page::Script => script_view::render(frame, app, snapshot, page_area),
|
||||
}
|
||||
|
||||
if let Some(side_area) = panel_area {
|
||||
render_side_panel(frame, app, side_area);
|
||||
Page::Main => main_view::render(frame, app, snapshot, body_area),
|
||||
Page::Patterns => patterns_view::render(frame, app, snapshot, body_area),
|
||||
Page::Engine => engine_view::render(frame, app, link, body_area),
|
||||
Page::Options => options_view::render(frame, app, body_area),
|
||||
Page::Help => help_view::render(frame, app, body_area),
|
||||
Page::Dict => dict_view::render(frame, app, body_area),
|
||||
Page::Script => script_view::render(frame, app, snapshot, body_area),
|
||||
Page::SampleExplorer => sample_explorer_view::render(frame, app, body_area),
|
||||
}
|
||||
|
||||
if !perf {
|
||||
@@ -292,69 +278,6 @@ fn header_height(_width: u16) -> u16 {
|
||||
3
|
||||
}
|
||||
|
||||
fn render_side_panel(frame: &mut Frame, app: &App, area: Rect) {
|
||||
let focused = app.panel.focus == PanelFocus::Side;
|
||||
match &app.panel.side {
|
||||
Some(SidePanel::SampleBrowser(state)) => {
|
||||
let [tree_area, preview_area] =
|
||||
Layout::vertical([Constraint::Fill(1), Constraint::Length(6)]).areas(area);
|
||||
|
||||
// Compute visible height: tree_area minus borders (2), minus search bar (1) if shown
|
||||
let mut vh = tree_area.height.saturating_sub(2) as usize;
|
||||
if state.search_active || !state.search_query.is_empty() {
|
||||
vh = vh.saturating_sub(1);
|
||||
}
|
||||
state.visible_height.set(vh);
|
||||
|
||||
let entries = state.entries();
|
||||
SampleBrowser::new(&entries, state.cursor)
|
||||
.scroll_offset(state.scroll_offset)
|
||||
.search(&state.search_query, state.search_active)
|
||||
.focused(focused)
|
||||
.render(frame, tree_area);
|
||||
|
||||
if let Some(sample) = state
|
||||
.sample_key()
|
||||
.and_then(|key| app.audio.sample_registry.as_ref()?.get(&key))
|
||||
.filter(|s| s.frame_count >= s.total_frames)
|
||||
{
|
||||
use crate::widgets::Waveform;
|
||||
use std::cell::RefCell;
|
||||
thread_local! {
|
||||
static MONO_BUF: RefCell<Vec<f32>> = const { RefCell::new(Vec::new()) };
|
||||
}
|
||||
|
||||
let [wave_area, info_area] =
|
||||
Layout::vertical([Constraint::Fill(1), Constraint::Length(1)])
|
||||
.areas(preview_area);
|
||||
|
||||
MONO_BUF.with(|buf| {
|
||||
let mut buf = buf.borrow_mut();
|
||||
let channels = sample.channels as usize;
|
||||
let frame_count = sample.frame_count as usize;
|
||||
buf.clear();
|
||||
buf.reserve(frame_count);
|
||||
for i in 0..frame_count {
|
||||
buf.push(sample.frames[i * channels]);
|
||||
}
|
||||
frame.render_widget(Waveform::new(&buf), wave_area);
|
||||
});
|
||||
|
||||
let duration = sample.total_frames as f32 / app.audio.config.sample_rate;
|
||||
let ch_label = if sample.channels == 1 {
|
||||
"mono"
|
||||
} else {
|
||||
"stereo"
|
||||
};
|
||||
let info = Paragraph::new(format!(" {duration:.1}s · {ch_label}"))
|
||||
.style(Style::new().fg(theme::get().ui.text_dim));
|
||||
frame.render_widget(info, info_area);
|
||||
}
|
||||
}
|
||||
None => {}
|
||||
}
|
||||
}
|
||||
|
||||
fn render_header(
|
||||
frame: &mut Frame,
|
||||
app: &App,
|
||||
@@ -527,6 +450,7 @@ fn render_footer(frame: &mut Frame, app: &App, snapshot: &SequencerSnapshot, are
|
||||
Page::Help => " HELP ",
|
||||
Page::Dict => " DICT ",
|
||||
Page::Script => " SCRIPT ",
|
||||
Page::SampleExplorer => " SAMPLES ",
|
||||
};
|
||||
|
||||
let content = if let Some(ref msg) = app.ui.status_message {
|
||||
@@ -549,10 +473,10 @@ fn render_footer(frame: &mut Frame, app: &App, snapshot: &SequencerSnapshot, are
|
||||
])
|
||||
} else {
|
||||
let bindings: Vec<(&str, &str)> = match app.page {
|
||||
Page::Main if app.panel.visible && app.panel.focus == PanelFocus::Side => vec![
|
||||
("↑↓", "Navigate"),
|
||||
("→", "Expand/Play"),
|
||||
("←", "Collapse"),
|
||||
Page::SampleExplorer => vec![
|
||||
("\u{2191}\u{2193}", "Navigate"),
|
||||
("\u{2192}", "Expand/Play"),
|
||||
("\u{2190}", "Collapse"),
|
||||
("/", "Search"),
|
||||
("Tab", "Close"),
|
||||
],
|
||||
|
||||
87
src/views/sample_explorer_view.rs
Normal file
87
src/views/sample_explorer_view.rs
Normal file
@@ -0,0 +1,87 @@
|
||||
use ratatui::layout::{Constraint, Layout, Rect};
|
||||
use ratatui::style::Style;
|
||||
use ratatui::Frame;
|
||||
|
||||
use crate::app::App;
|
||||
use crate::theme;
|
||||
use crate::widgets::{SampleBrowser, Waveform};
|
||||
|
||||
pub fn render(frame: &mut Frame, app: &App, area: Rect) {
|
||||
render_browser(frame, app, area);
|
||||
}
|
||||
|
||||
fn render_browser(frame: &mut Frame, app: &App, area: Rect) {
|
||||
let state = match &app.sample_browser {
|
||||
Some(s) => s,
|
||||
None => return,
|
||||
};
|
||||
|
||||
let [tree_area, preview_area] =
|
||||
Layout::vertical([Constraint::Fill(1), Constraint::Length(6)]).areas(area);
|
||||
|
||||
let mut vh = tree_area.height.saturating_sub(2) as usize;
|
||||
if state.search_active || !state.search_query.is_empty() {
|
||||
vh = vh.saturating_sub(1);
|
||||
}
|
||||
state.visible_height.set(vh);
|
||||
|
||||
let entries = state.entries();
|
||||
SampleBrowser::new(&entries, state.cursor)
|
||||
.scroll_offset(state.scroll_offset)
|
||||
.search(&state.search_query, state.search_active)
|
||||
.focused(true)
|
||||
.render(frame, tree_area);
|
||||
|
||||
render_waveform_preview(frame, app, state, preview_area);
|
||||
}
|
||||
|
||||
fn render_waveform_preview(
|
||||
frame: &mut Frame,
|
||||
app: &App,
|
||||
state: &crate::state::SampleBrowserState,
|
||||
area: Rect,
|
||||
) {
|
||||
use ratatui::text::{Line, Span};
|
||||
use ratatui::widgets::Paragraph;
|
||||
use std::cell::RefCell;
|
||||
|
||||
let sample = match state
|
||||
.sample_key()
|
||||
.and_then(|key| app.audio.sample_registry.as_ref()?.get(&key))
|
||||
.filter(|s| s.frame_count >= s.total_frames)
|
||||
{
|
||||
Some(s) => s,
|
||||
None => return,
|
||||
};
|
||||
|
||||
thread_local! {
|
||||
static MONO_BUF: RefCell<Vec<f32>> = const { RefCell::new(Vec::new()) };
|
||||
}
|
||||
|
||||
let [wave_area, info_area] =
|
||||
Layout::vertical([Constraint::Fill(1), Constraint::Length(1)]).areas(area);
|
||||
|
||||
MONO_BUF.with(|buf| {
|
||||
let mut buf = buf.borrow_mut();
|
||||
let channels = sample.channels as usize;
|
||||
let frame_count = sample.frame_count as usize;
|
||||
buf.clear();
|
||||
buf.reserve(frame_count);
|
||||
for i in 0..frame_count {
|
||||
buf.push(sample.frames[i * channels]);
|
||||
}
|
||||
frame.render_widget(Waveform::new(&buf), wave_area);
|
||||
});
|
||||
|
||||
let duration = sample.total_frames as f32 / app.audio.config.sample_rate;
|
||||
let ch_label = if sample.channels == 1 {
|
||||
"mono"
|
||||
} else {
|
||||
"stereo"
|
||||
};
|
||||
let info = Paragraph::new(Line::from(Span::styled(
|
||||
format!(" {duration:.1}s \u{00B7} {ch_label}"),
|
||||
Style::new().fg(theme::get().ui.text_dim),
|
||||
)));
|
||||
frame.render_widget(info, info_area);
|
||||
}
|
||||
@@ -23,7 +23,7 @@ fn redefine_word_overwrites() {
|
||||
|
||||
#[test]
|
||||
fn word_with_param() {
|
||||
let outputs = expect_outputs(": loud 0.9 gain ; \"kick\" s loud .", 1);
|
||||
let outputs = expect_outputs(": loud 0.9 gain ; \"kick\" snd loud .", 1);
|
||||
assert!(outputs[0].contains("gain/0.9"));
|
||||
}
|
||||
|
||||
@@ -98,7 +98,7 @@ fn define_word_containing_quotation() {
|
||||
|
||||
#[test]
|
||||
fn define_word_with_sound() {
|
||||
let outputs = expect_outputs(": kick \"kick\" s . ; kick", 1);
|
||||
let outputs = expect_outputs(": kick \"kick\" snd . ; kick", 1);
|
||||
assert!(outputs[0].contains("sound/kick"));
|
||||
}
|
||||
|
||||
|
||||
@@ -8,14 +8,14 @@ use cagire::forth::Value;
|
||||
|
||||
#[test]
|
||||
fn test_midi_channel_set() {
|
||||
let outputs = expect_outputs("60 note 100 velocity 3 chan m.", 1);
|
||||
assert!(outputs[0].starts_with("/midi/note/60/vel/100/chan/2/dur/"));
|
||||
let outputs = expect_outputs("60 note 0.8 velocity 3 chan m.", 1);
|
||||
assert!(outputs[0].starts_with("/midi/note/60/vel/101/chan/2/dur/"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_midi_note_default_channel() {
|
||||
let outputs = expect_outputs("72 note 80 velocity m.", 1);
|
||||
assert!(outputs[0].starts_with("/midi/note/72/vel/80/chan/0/dur/"));
|
||||
let outputs = expect_outputs("72 note 0.6 velocity m.", 1);
|
||||
assert!(outputs[0].starts_with("/midi/note/72/vel/76/chan/0/dur/"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
@@ -79,43 +79,43 @@ fn test_ccval_reads_from_cc_memory() {
|
||||
#[test]
|
||||
fn test_midi_channel_clamping() {
|
||||
// Channel should be clamped 1-16, then converted to 0-15 internally
|
||||
let outputs = expect_outputs("60 note 100 velocity 0 chan m.", 1);
|
||||
let outputs = expect_outputs("60 note 0.8 velocity 0 chan m.", 1);
|
||||
assert!(outputs[0].contains("/chan/0")); // 0 clamped to 1, then -1 = 0
|
||||
|
||||
let outputs = expect_outputs("60 note 100 velocity 17 chan m.", 1);
|
||||
let outputs = expect_outputs("60 note 0.8 velocity 17 chan m.", 1);
|
||||
assert!(outputs[0].contains("/chan/15")); // 17 clamped to 16, then -1 = 15
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_midi_note_clamping() {
|
||||
let outputs = expect_outputs("-1 note 100 velocity m.", 1);
|
||||
let outputs = expect_outputs("-1 note 0.8 velocity m.", 1);
|
||||
assert!(outputs[0].contains("/note/0"));
|
||||
|
||||
let outputs = expect_outputs("200 note 100 velocity m.", 1);
|
||||
let outputs = expect_outputs("200 note 0.8 velocity m.", 1);
|
||||
assert!(outputs[0].contains("/note/127"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_midi_velocity_clamping() {
|
||||
let outputs = expect_outputs("60 note -10 velocity m.", 1);
|
||||
let outputs = expect_outputs("60 note -0.1 velocity m.", 1);
|
||||
assert!(outputs[0].contains("/vel/0"));
|
||||
|
||||
let outputs = expect_outputs("60 note 200 velocity m.", 1);
|
||||
let outputs = expect_outputs("60 note 2.0 velocity m.", 1);
|
||||
assert!(outputs[0].contains("/vel/127"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_midi_defaults() {
|
||||
// With only note specified, velocity defaults to 100 and channel to 0
|
||||
// With only note specified, velocity defaults to 0.8 (101) and channel to 0
|
||||
let outputs = expect_outputs("60 note m.", 1);
|
||||
assert!(outputs[0].starts_with("/midi/note/60/vel/100/chan/0/dur/"));
|
||||
assert!(outputs[0].starts_with("/midi/note/60/vel/101/chan/0/dur/"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_midi_full_defaults() {
|
||||
// With nothing specified, defaults to note=60, velocity=100, channel=0
|
||||
// With nothing specified, defaults to note=60, velocity=0.8 (101), channel=0
|
||||
let outputs = expect_outputs("m.", 1);
|
||||
assert!(outputs[0].starts_with("/midi/note/60/vel/100/chan/0/dur/"));
|
||||
assert!(outputs[0].starts_with("/midi/note/60/vel/101/chan/0/dur/"));
|
||||
}
|
||||
|
||||
// Pitch bend tests
|
||||
@@ -344,10 +344,10 @@ fn test_midi_polyphonic_notes() {
|
||||
|
||||
#[test]
|
||||
fn test_midi_polyphonic_notes_with_velocity() {
|
||||
let outputs = expect_outputs("60 64 67 note 100 80 60 velocity m.", 3);
|
||||
assert!(outputs[0].contains("/note/60/vel/100/"));
|
||||
assert!(outputs[1].contains("/note/64/vel/80/"));
|
||||
assert!(outputs[2].contains("/note/67/vel/60/"));
|
||||
let outputs = expect_outputs("60 64 67 note 0.8 0.6 0.5 velocity m.", 3);
|
||||
assert!(outputs[0].contains("/note/60/vel/101/"));
|
||||
assert!(outputs[1].contains("/note/64/vel/76/"));
|
||||
assert!(outputs[2].contains("/note/67/vel/63/"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
|
||||
@@ -59,20 +59,20 @@ fn nested_quotations() {
|
||||
|
||||
#[test]
|
||||
fn quotation_with_param() {
|
||||
let outputs = expect_outputs(r#""kick" s ( 2 distort ) 1 ? ."#, 1);
|
||||
let outputs = expect_outputs(r#""kick" snd ( 2 distort ) 1 ? ."#, 1);
|
||||
assert!(outputs[0].contains("distort/2"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn quotation_skips_param() {
|
||||
let outputs = expect_outputs(r#""kick" s ( 2 distort ) 0 ? ."#, 1);
|
||||
let outputs = expect_outputs(r#""kick" snd ( 2 distort ) 0 ? ."#, 1);
|
||||
assert!(!outputs[0].contains("distort"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn quotation_with_emit() {
|
||||
// When true, . should fire
|
||||
let outputs = expect_outputs(r#""kick" s ( . ) 1 ?"#, 1);
|
||||
let outputs = expect_outputs(r#""kick" snd ( . ) 1 ?"#, 1);
|
||||
assert!(outputs[0].contains("kick"));
|
||||
}
|
||||
|
||||
@@ -81,7 +81,7 @@ fn quotation_skips_emit() {
|
||||
// When false, . should not fire
|
||||
let f = forth();
|
||||
let outputs = f
|
||||
.evaluate(r#""kick" s ( . ) 0 ?"#, &default_ctx())
|
||||
.evaluate(r#""kick" snd ( . ) 0 ?"#, &default_ctx())
|
||||
.unwrap();
|
||||
// No output since . was skipped and no implicit emit
|
||||
assert_eq!(outputs.len(), 0);
|
||||
@@ -109,7 +109,7 @@ fn every_with_quotation_integration() {
|
||||
let ctx = ctx_with(|c| c.iter = iter);
|
||||
let f = forth();
|
||||
let outputs = f
|
||||
.evaluate(r#""kick" s ( 2 distort ) 2 every ."#, &ctx)
|
||||
.evaluate(r#""kick" snd ( 2 distort ) 2 every ."#, &ctx)
|
||||
.unwrap();
|
||||
if iter % 2 == 0 {
|
||||
assert!(
|
||||
@@ -134,7 +134,7 @@ fn bjork_with_sound() {
|
||||
let ctx = ctx_with(|c| c.runs = 2); // position 2 is a hit for (3,8)
|
||||
let f = forth();
|
||||
let outputs = f
|
||||
.evaluate(r#""kick" s ( 2 distort ) 3 8 bjork ."#, &ctx)
|
||||
.evaluate(r#""kick" snd ( 2 distort ) 3 8 bjork ."#, &ctx)
|
||||
.unwrap();
|
||||
assert!(outputs[0].contains("distort/2"));
|
||||
}
|
||||
@@ -161,7 +161,7 @@ fn when_and_unless_complementary() {
|
||||
let f = forth();
|
||||
let outputs = f
|
||||
.evaluate(
|
||||
r#""kick" s ( 2 distort ) iter 2 mod 0 = ? ( 4 distort ) iter 2 mod 0 = !? ."#,
|
||||
r#""kick" snd ( 2 distort ) iter 2 mod 0 = ? ( 4 distort ) iter 2 mod 0 = !? ."#,
|
||||
&ctx,
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
@@ -8,13 +8,13 @@ fn basic_emit() {
|
||||
|
||||
#[test]
|
||||
fn alias_s() {
|
||||
let outputs = expect_outputs(r#""snare" s ."#, 1);
|
||||
let outputs = expect_outputs(r#""snare" snd ."#, 1);
|
||||
assert!(outputs[0].contains("sound/snare"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn with_params() {
|
||||
let outputs = expect_outputs(r#""kick" s 440 freq 0.5 gain ."#, 1);
|
||||
let outputs = expect_outputs(r#""kick" snd 440 freq 0.5 gain ."#, 1);
|
||||
assert!(outputs[0].contains("sound/kick"));
|
||||
assert!(outputs[0].contains("freq/440"));
|
||||
assert!(outputs[0].contains("gain/0.5"));
|
||||
@@ -22,13 +22,13 @@ fn with_params() {
|
||||
|
||||
#[test]
|
||||
fn auto_dur() {
|
||||
let outputs = expect_outputs(r#""kick" s ."#, 1);
|
||||
let outputs = expect_outputs(r#""kick" snd ."#, 1);
|
||||
assert!(outputs[0].contains("dur/"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn auto_delaytime() {
|
||||
let outputs = expect_outputs(r#""kick" s ."#, 1);
|
||||
let outputs = expect_outputs(r#""kick" snd ."#, 1);
|
||||
assert!(outputs[0].contains("delaytime/"));
|
||||
}
|
||||
|
||||
@@ -39,7 +39,7 @@ fn emit_no_sound() {
|
||||
|
||||
#[test]
|
||||
fn multiple_emits() {
|
||||
let outputs = expect_outputs(r#""kick" s . "snare" s ."#, 2);
|
||||
let outputs = expect_outputs(r#""kick" snd . "snare" snd ."#, 2);
|
||||
assert!(outputs[0].contains("sound/kick"));
|
||||
assert!(outputs[1].contains("sound/snare"));
|
||||
}
|
||||
@@ -48,7 +48,7 @@ fn multiple_emits() {
|
||||
fn envelope_params() {
|
||||
// Values are tempo-scaled: 0.01 * step_duration(0.125) = 0.00125, etc.
|
||||
let outputs = expect_outputs(
|
||||
r#""synth" s 0.01 attack 0.1 decay 0.7 sustain 0.3 release ."#,
|
||||
r#""synth" snd 0.01 attack 0.1 decay 0.7 sustain 0.3 release ."#,
|
||||
1,
|
||||
);
|
||||
assert!(outputs[0].contains("attack/0.00125"));
|
||||
@@ -59,14 +59,14 @@ fn envelope_params() {
|
||||
|
||||
#[test]
|
||||
fn filter_params() {
|
||||
let outputs = expect_outputs(r#""synth" s 2000 lpf 0.5 lpq ."#, 1);
|
||||
let outputs = expect_outputs(r#""synth" snd 2000 lpf 0.5 lpq ."#, 1);
|
||||
assert!(outputs[0].contains("lpf/2000"));
|
||||
assert!(outputs[0].contains("lpq/0.5"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn adsr_sets_all_envelope_params() {
|
||||
let outputs = expect_outputs(r#""synth" s 0.01 0.1 0.5 0.3 adsr ."#, 1);
|
||||
let outputs = expect_outputs(r#""synth" snd 0.01 0.1 0.5 0.3 adsr ."#, 1);
|
||||
assert!(outputs[0].contains("attack/0.00125"));
|
||||
assert!(outputs[0].contains("decay/0.0125"));
|
||||
assert!(outputs[0].contains("sustain/0.5"));
|
||||
@@ -75,7 +75,7 @@ fn adsr_sets_all_envelope_params() {
|
||||
|
||||
#[test]
|
||||
fn ad_sets_attack_decay_sustain_zero() {
|
||||
let outputs = expect_outputs(r#""synth" s 0.01 0.1 ad ."#, 1);
|
||||
let outputs = expect_outputs(r#""synth" snd 0.01 0.1 ad ."#, 1);
|
||||
assert!(outputs[0].contains("attack/0.00125"));
|
||||
assert!(outputs[0].contains("decay/0.0125"));
|
||||
assert!(outputs[0].contains("sustain/0"));
|
||||
@@ -83,7 +83,7 @@ fn ad_sets_attack_decay_sustain_zero() {
|
||||
|
||||
#[test]
|
||||
fn bank_param() {
|
||||
let outputs = expect_outputs(r#""loop" s "a" bank ."#, 1);
|
||||
let outputs = expect_outputs(r#""loop" snd "a" bank ."#, 1);
|
||||
assert!(outputs[0].contains("sound/loop"));
|
||||
assert!(outputs[0].contains("bank/a"));
|
||||
}
|
||||
@@ -109,7 +109,7 @@ fn param_only_multiple_params() {
|
||||
|
||||
#[test]
|
||||
fn polyphonic_notes() {
|
||||
let outputs = expect_outputs(r#"60 64 67 note sine s ."#, 3);
|
||||
let outputs = expect_outputs(r#"60 64 67 note sine snd ."#, 3);
|
||||
assert!(outputs[0].contains("note/60"));
|
||||
assert!(outputs[1].contains("note/64"));
|
||||
assert!(outputs[2].contains("note/67"));
|
||||
@@ -117,14 +117,14 @@ fn polyphonic_notes() {
|
||||
|
||||
#[test]
|
||||
fn polyphonic_sounds() {
|
||||
let outputs = expect_outputs(r#"440 freq kick hat s ."#, 2);
|
||||
let outputs = expect_outputs(r#"440 freq kick hat snd ."#, 2);
|
||||
assert!(outputs[0].contains("sound/kick"));
|
||||
assert!(outputs[1].contains("sound/hat"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn polyphonic_cycling() {
|
||||
let outputs = expect_outputs(r#"60 64 67 note 0.5 1.0 gain sine s ."#, 3);
|
||||
let outputs = expect_outputs(r#"60 64 67 note 0.5 1.0 gain sine snd ."#, 3);
|
||||
assert!(outputs[0].contains("note/60"));
|
||||
assert!(outputs[0].contains("gain/0.5"));
|
||||
assert!(outputs[1].contains("note/64"));
|
||||
@@ -135,7 +135,7 @@ fn polyphonic_cycling() {
|
||||
|
||||
#[test]
|
||||
fn polyphonic_with_at() {
|
||||
let outputs = expect_outputs(r#"0 0.5 at 60 64 note sine s ."#, 4);
|
||||
let outputs = expect_outputs(r#"0 0.5 at 60 64 note sine snd ."#, 4);
|
||||
assert_eq!(outputs.len(), 4);
|
||||
}
|
||||
|
||||
@@ -148,7 +148,7 @@ fn explicit_dur_zero_is_infinite() {
|
||||
#[test]
|
||||
fn all_before_sounds() {
|
||||
let outputs = expect_outputs(
|
||||
r#"500 lpf 0.5 verb all "kick" s 60 note . "hat" s 70 note ."#,
|
||||
r#"500 lpf 0.5 verb all "kick" snd 60 note . "hat" snd 70 note ."#,
|
||||
2,
|
||||
);
|
||||
assert!(outputs[0].contains("sound/kick"));
|
||||
@@ -162,7 +162,7 @@ fn all_before_sounds() {
|
||||
#[test]
|
||||
fn all_after_sounds() {
|
||||
let outputs = expect_outputs(
|
||||
r#""kick" s 60 note . "hat" s 70 note . 500 lpf 0.5 verb all"#,
|
||||
r#""kick" snd 60 note . "hat" snd 70 note . 500 lpf 0.5 verb all"#,
|
||||
2,
|
||||
);
|
||||
assert!(outputs[0].contains("sound/kick"));
|
||||
@@ -176,7 +176,7 @@ fn all_after_sounds() {
|
||||
#[test]
|
||||
fn noall_clears_global_params() {
|
||||
let outputs = expect_outputs(
|
||||
r#"500 lpf all "kick" s 60 note . noall "hat" s 70 note ."#,
|
||||
r#"500 lpf all "kick" snd 60 note . noall "hat" snd 70 note ."#,
|
||||
2,
|
||||
);
|
||||
assert!(outputs[0].contains("lpf/500"));
|
||||
@@ -187,7 +187,7 @@ fn noall_clears_global_params() {
|
||||
fn all_with_tempo_scaled_params() {
|
||||
// attack is tempo-scaled: 0.01 * step_duration(0.125) = 0.00125
|
||||
let outputs = expect_outputs(
|
||||
r#"0.01 attack all "kick" s 60 note ."#,
|
||||
r#"0.01 attack all "kick" snd 60 note ."#,
|
||||
1,
|
||||
);
|
||||
assert!(outputs[0].contains("attack/0.00125"));
|
||||
@@ -196,7 +196,7 @@ fn all_with_tempo_scaled_params() {
|
||||
#[test]
|
||||
fn all_per_sound_override() {
|
||||
let outputs = expect_outputs(
|
||||
r#"500 lpf all "kick" s 2000 lpf . "hat" s ."#,
|
||||
r#"500 lpf all "kick" snd 2000 lpf . "hat" snd ."#,
|
||||
2,
|
||||
);
|
||||
// kick has both global lpf=500 and per-sound lpf=2000; per-sound wins (comes last)
|
||||
@@ -210,7 +210,7 @@ fn all_persists_across_evaluations() {
|
||||
let f = forth();
|
||||
let ctx = default_ctx();
|
||||
f.evaluate(r#"500 lpf 0.5 verb all"#, &ctx).unwrap();
|
||||
let outputs = f.evaluate(r#""kick" s 60 note ."#, &ctx).unwrap();
|
||||
let outputs = f.evaluate(r#""kick" snd 60 note ."#, &ctx).unwrap();
|
||||
assert_eq!(outputs.len(), 1);
|
||||
assert!(outputs[0].contains("lpf/500"), "global lpf missing: {}", outputs[0]);
|
||||
assert!(outputs[0].contains("verb/0.5"), "global verb missing: {}", outputs[0]);
|
||||
@@ -222,7 +222,7 @@ fn noall_clears_across_evaluations() {
|
||||
let ctx = default_ctx();
|
||||
f.evaluate(r#"500 lpf all"#, &ctx).unwrap();
|
||||
f.evaluate(r#"noall"#, &ctx).unwrap();
|
||||
let outputs = f.evaluate(r#""kick" s 60 note ."#, &ctx).unwrap();
|
||||
let outputs = f.evaluate(r#""kick" snd 60 note ."#, &ctx).unwrap();
|
||||
assert_eq!(outputs.len(), 1);
|
||||
assert!(!outputs[0].contains("lpf"), "lpf should be cleared: {}", outputs[0]);
|
||||
}
|
||||
@@ -251,20 +251,20 @@ fn all_replaces_previous_global() {
|
||||
let ctx = default_ctx();
|
||||
f.evaluate(r#"500 lpf 0.5 verb all"#, &ctx).unwrap();
|
||||
f.evaluate(r#"2000 lpf all"#, &ctx).unwrap();
|
||||
let outputs = f.evaluate(r#""kick" s ."#, &ctx).unwrap();
|
||||
let outputs = f.evaluate(r#""kick" snd ."#, &ctx).unwrap();
|
||||
assert_eq!(outputs.len(), 1);
|
||||
assert!(outputs[0].contains("lpf/2000"), "latest lpf should be 2000: {}", outputs[0]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn slice_param() {
|
||||
let outputs = expect_outputs(r#""break" s 8 slice ."#, 1);
|
||||
let outputs = expect_outputs(r#""break" snd 8 slice ."#, 1);
|
||||
assert!(outputs[0].contains("slice/8"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn pick_param() {
|
||||
let outputs = expect_outputs(r#""break" s 8 slice 3 pick ."#, 1);
|
||||
let outputs = expect_outputs(r#""break" snd 8 slice 3 pick ."#, 1);
|
||||
assert!(outputs[0].contains("slice/8"));
|
||||
assert!(outputs[0].contains("pick/3"));
|
||||
}
|
||||
|
||||
@@ -56,14 +56,14 @@ fn stepdur_baseline() {
|
||||
|
||||
#[test]
|
||||
fn single_emit() {
|
||||
let outputs = expect_outputs(r#""kick" s ."#, 1);
|
||||
let outputs = expect_outputs(r#""kick" snd ."#, 1);
|
||||
let deltas = get_deltas(&outputs);
|
||||
assert!(approx_eq(deltas[0], 0.0), "single emit at start should have delta 0");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn multiple_emits_all_at_zero() {
|
||||
let outputs = expect_outputs(r#""kick" s . . . ."#, 4);
|
||||
let outputs = expect_outputs(r#""kick" snd . . . ."#, 4);
|
||||
let deltas = get_deltas(&outputs);
|
||||
for (i, delta) in deltas.iter().enumerate() {
|
||||
assert!(approx_eq(*delta, 0.0), "emit {}: expected delta 0, got {}", i, delta);
|
||||
@@ -72,7 +72,7 @@ fn multiple_emits_all_at_zero() {
|
||||
|
||||
#[test]
|
||||
fn sound_persists() {
|
||||
let outputs = expect_outputs(r#""kick" s . . "hat" s . ."#, 4);
|
||||
let outputs = expect_outputs(r#""kick" snd . . "hat" snd . ."#, 4);
|
||||
let sounds = get_sounds(&outputs);
|
||||
assert_eq!(sounds[0], "kick");
|
||||
assert_eq!(sounds[1], "kick");
|
||||
@@ -82,14 +82,14 @@ fn sound_persists() {
|
||||
|
||||
#[test]
|
||||
fn alternating_sounds() {
|
||||
let outputs = expect_outputs(r#""kick" s . "snare" s . "kick" s . "snare" s ."#, 4);
|
||||
let outputs = expect_outputs(r#""kick" snd . "snare" snd . "kick" snd . "snare" snd ."#, 4);
|
||||
let sounds = get_sounds(&outputs);
|
||||
assert_eq!(sounds, vec!["kick", "snare", "kick", "snare"]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn dur_is_step_duration() {
|
||||
let outputs = expect_outputs(r#""kick" s ."#, 1);
|
||||
let outputs = expect_outputs(r#""kick" snd ."#, 1);
|
||||
let durs = get_durs(&outputs);
|
||||
assert!(approx_eq(durs[0], 0.5), "dur should be 4 * step_duration (0.5), got {}", durs[0]);
|
||||
}
|
||||
@@ -99,7 +99,7 @@ fn cycle_picks_by_runs() {
|
||||
for runs in 0..4 {
|
||||
let ctx = ctx_with(|c| c.runs = runs);
|
||||
let f = forth();
|
||||
let outputs = f.evaluate(r#""kick" s ( . ) ( ) 2 cycle"#, &ctx).unwrap();
|
||||
let outputs = f.evaluate(r#""kick" snd ( . ) ( ) 2 cycle"#, &ctx).unwrap();
|
||||
if runs % 2 == 0 {
|
||||
assert_eq!(outputs.len(), 1, "runs={}: emit should be picked", runs);
|
||||
} else {
|
||||
@@ -113,7 +113,7 @@ fn pcycle_picks_by_iter() {
|
||||
for iter in 0..4 {
|
||||
let ctx = ctx_with(|c| c.iter = iter);
|
||||
let f = forth();
|
||||
let outputs = f.evaluate(r#""kick" s ( . ) ( ) 2 pcycle"#, &ctx).unwrap();
|
||||
let outputs = f.evaluate(r#""kick" snd ( . ) ( ) 2 pcycle"#, &ctx).unwrap();
|
||||
if iter % 2 == 0 {
|
||||
assert_eq!(outputs.len(), 1, "iter={}: emit should be picked", iter);
|
||||
} else {
|
||||
@@ -128,7 +128,7 @@ fn cycle_with_sounds() {
|
||||
let ctx = ctx_with(|c| c.runs = runs);
|
||||
let f = forth();
|
||||
let outputs = f.evaluate(
|
||||
r#"( "kick" s . ) ( "hat" s . ) ( "snare" s . ) 3 cycle"#,
|
||||
r#"( "kick" snd . ) ( "hat" snd . ) ( "snare" snd . ) 3 cycle"#,
|
||||
&ctx
|
||||
).unwrap();
|
||||
assert_eq!(outputs.len(), 1, "runs={}: expected 1 output", runs);
|
||||
@@ -141,7 +141,7 @@ fn cycle_with_sounds() {
|
||||
|
||||
#[test]
|
||||
fn at_single_delta() {
|
||||
let outputs = expect_outputs(r#"0.5 at "kick" s ."#, 1);
|
||||
let outputs = expect_outputs(r#"0.5 at "kick" snd ."#, 1);
|
||||
let deltas = get_deltas(&outputs);
|
||||
let step_dur = 0.125;
|
||||
assert!(approx_eq(deltas[0], 0.5 * step_dur), "expected delta at 0.5 of step, got {}", deltas[0]);
|
||||
@@ -149,7 +149,7 @@ fn at_single_delta() {
|
||||
|
||||
#[test]
|
||||
fn at_list_deltas() {
|
||||
let outputs = expect_outputs(r#"0 0.5 at "kick" s ."#, 2);
|
||||
let outputs = expect_outputs(r#"0 0.5 at "kick" snd ."#, 2);
|
||||
let deltas = get_deltas(&outputs);
|
||||
let step_dur = 0.125;
|
||||
assert!(approx_eq(deltas[0], 0.0), "expected delta 0, got {}", deltas[0]);
|
||||
@@ -158,7 +158,7 @@ fn at_list_deltas() {
|
||||
|
||||
#[test]
|
||||
fn at_three_deltas() {
|
||||
let outputs = expect_outputs(r#"0 0.33 0.67 at "kick" s ."#, 3);
|
||||
let outputs = expect_outputs(r#"0 0.33 0.67 at "kick" snd ."#, 3);
|
||||
let deltas = get_deltas(&outputs);
|
||||
let step_dur = 0.125;
|
||||
assert!(approx_eq(deltas[0], 0.0), "expected delta 0");
|
||||
@@ -168,7 +168,7 @@ fn at_three_deltas() {
|
||||
|
||||
#[test]
|
||||
fn at_persists_across_emits() {
|
||||
let outputs = expect_outputs(r#"0 0.5 at "kick" s . "hat" s ."#, 4);
|
||||
let outputs = expect_outputs(r#"0 0.5 at "kick" snd . "hat" snd ."#, 4);
|
||||
let sounds = get_sounds(&outputs);
|
||||
assert_eq!(sounds, vec!["kick", "kick", "hat", "hat"]);
|
||||
}
|
||||
@@ -176,14 +176,14 @@ fn at_persists_across_emits() {
|
||||
|
||||
#[test]
|
||||
fn at_reset_with_zero() {
|
||||
let outputs = expect_outputs(r#"0 0.5 at "kick" s . 0.0 at "hat" s ."#, 3);
|
||||
let outputs = expect_outputs(r#"0 0.5 at "kick" snd . 0.0 at "hat" snd ."#, 3);
|
||||
let sounds = get_sounds(&outputs);
|
||||
assert_eq!(sounds, vec!["kick", "kick", "hat"]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn clear_resets_at_deltas() {
|
||||
let outputs = expect_outputs(r#"0 0.5 at "kick" s . clear "hat" s ."#, 3);
|
||||
let outputs = expect_outputs(r#"0 0.5 at "kick" snd . clear "hat" snd ."#, 3);
|
||||
let sounds = get_sounds(&outputs);
|
||||
assert_eq!(sounds, vec!["kick", "kick", "hat"]);
|
||||
let deltas = get_deltas(&outputs);
|
||||
@@ -196,7 +196,7 @@ fn at_records_selected_spans() {
|
||||
|
||||
let f = forth();
|
||||
let mut trace = ExecutionTrace::default();
|
||||
let script = r#"0 0.5 0.75 at "kick" s ."#;
|
||||
let script = r#"0 0.5 0.75 at "kick" snd ."#;
|
||||
f.evaluate_with_trace(script, &default_ctx(), &mut trace).unwrap();
|
||||
|
||||
// Should have 6 selected spans: 3 for at deltas + 3 for sound (one per emit)
|
||||
@@ -226,7 +226,7 @@ fn get_gains(outputs: &[String]) -> Vec<f64> {
|
||||
|
||||
#[test]
|
||||
fn arp_auto_subdivide() {
|
||||
let outputs = expect_outputs(r#"sine s c4 e4 g4 b4 arp note ."#, 4);
|
||||
let outputs = expect_outputs(r#"sine snd c4 e4 g4 b4 arp note ."#, 4);
|
||||
let notes = get_notes(&outputs);
|
||||
assert!(approx_eq(notes[0], 60.0));
|
||||
assert!(approx_eq(notes[1], 64.0));
|
||||
@@ -242,7 +242,7 @@ fn arp_auto_subdivide() {
|
||||
|
||||
#[test]
|
||||
fn arp_with_explicit_at() {
|
||||
let outputs = expect_outputs(r#"0 0.25 0.5 0.75 at sine s c4 e4 g4 b4 arp note ."#, 4);
|
||||
let outputs = expect_outputs(r#"0 0.25 0.5 0.75 at sine snd c4 e4 g4 b4 arp note ."#, 4);
|
||||
let notes = get_notes(&outputs);
|
||||
assert!(approx_eq(notes[0], 60.0));
|
||||
assert!(approx_eq(notes[1], 64.0));
|
||||
@@ -258,14 +258,14 @@ fn arp_with_explicit_at() {
|
||||
|
||||
#[test]
|
||||
fn arp_single_note() {
|
||||
let outputs = expect_outputs(r#"sine s c4 arp note ."#, 1);
|
||||
let outputs = expect_outputs(r#"sine snd c4 arp note ."#, 1);
|
||||
let notes = get_notes(&outputs);
|
||||
assert!(approx_eq(notes[0], 60.0));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn arp_fewer_deltas_than_notes() {
|
||||
let outputs = expect_outputs(r#"0 0.5 at sine s c4 e4 g4 b4 arp note ."#, 4);
|
||||
let outputs = expect_outputs(r#"0 0.5 at sine snd c4 e4 g4 b4 arp note ."#, 4);
|
||||
let notes = get_notes(&outputs);
|
||||
assert!(approx_eq(notes[0], 60.0));
|
||||
assert!(approx_eq(notes[1], 64.0));
|
||||
@@ -281,7 +281,7 @@ fn arp_fewer_deltas_than_notes() {
|
||||
|
||||
#[test]
|
||||
fn arp_fewer_notes_than_deltas() {
|
||||
let outputs = expect_outputs(r#"0 0.25 0.5 0.75 at sine s c4 e4 arp note ."#, 4);
|
||||
let outputs = expect_outputs(r#"0 0.25 0.5 0.75 at sine snd c4 e4 arp note ."#, 4);
|
||||
let notes = get_notes(&outputs);
|
||||
assert!(approx_eq(notes[0], 60.0));
|
||||
assert!(approx_eq(notes[1], 64.0));
|
||||
@@ -291,7 +291,7 @@ fn arp_fewer_notes_than_deltas() {
|
||||
|
||||
#[test]
|
||||
fn arp_multiple_params() {
|
||||
let outputs = expect_outputs(r#"sine s c4 e4 g4 arp note 0.5 0.7 0.9 arp gain ."#, 3);
|
||||
let outputs = expect_outputs(r#"sine snd c4 e4 g4 arp note 0.5 0.7 0.9 arp gain ."#, 3);
|
||||
let notes = get_notes(&outputs);
|
||||
assert!(approx_eq(notes[0], 60.0));
|
||||
assert!(approx_eq(notes[1], 64.0));
|
||||
@@ -305,7 +305,7 @@ fn arp_multiple_params() {
|
||||
#[test]
|
||||
fn arp_no_arp_unchanged() {
|
||||
// Standard CycleList without arp → cross-product (backward compat)
|
||||
let outputs = expect_outputs(r#"0 0.5 at sine s c4 e4 note ."#, 4);
|
||||
let outputs = expect_outputs(r#"0 0.5 at sine snd c4 e4 note ."#, 4);
|
||||
let notes = get_notes(&outputs);
|
||||
// Cross-product: each note at each delta
|
||||
assert!(approx_eq(notes[0], 60.0));
|
||||
@@ -318,7 +318,7 @@ fn arp_no_arp_unchanged() {
|
||||
fn arp_mixed_cycle_and_arp() {
|
||||
// CycleList sound (2) + ArpList note (3) → 3 arp × 2 poly = 6 voices
|
||||
// Each arp step plays both sine and saw simultaneously (poly stacking)
|
||||
let outputs = expect_outputs(r#"sine saw s c4 e4 g4 arp note ."#, 6);
|
||||
let outputs = expect_outputs(r#"sine saw snd c4 e4 g4 arp note ."#, 6);
|
||||
let sounds = get_sounds(&outputs);
|
||||
// Arp step 0: poly 0=sine, poly 1=saw
|
||||
assert_eq!(sounds[0], "sine");
|
||||
@@ -346,7 +346,7 @@ fn every_offset_fires_at_offset() {
|
||||
for iter in 0..8 {
|
||||
let ctx = ctx_with(|c| c.iter = iter);
|
||||
let f = forth();
|
||||
let outputs = f.evaluate(r#""kick" s ( . ) 4 2 every+"#, &ctx).unwrap();
|
||||
let outputs = f.evaluate(r#""kick" snd ( . ) 4 2 every+"#, &ctx).unwrap();
|
||||
if iter % 4 == 2 {
|
||||
assert_eq!(outputs.len(), 1, "iter={}: should fire", iter);
|
||||
} else {
|
||||
@@ -361,7 +361,7 @@ fn every_offset_wraps_large_offset() {
|
||||
for iter in 0..8 {
|
||||
let ctx = ctx_with(|c| c.iter = iter);
|
||||
let f = forth();
|
||||
let outputs = f.evaluate(r#""kick" s ( . ) 4 6 every+"#, &ctx).unwrap();
|
||||
let outputs = f.evaluate(r#""kick" snd ( . ) 4 6 every+"#, &ctx).unwrap();
|
||||
if iter % 4 == 2 {
|
||||
assert_eq!(outputs.len(), 1, "iter={}: should fire (wrapped offset)", iter);
|
||||
} else {
|
||||
@@ -375,7 +375,7 @@ fn except_offset_inverse() {
|
||||
for iter in 0..8 {
|
||||
let ctx = ctx_with(|c| c.iter = iter);
|
||||
let f = forth();
|
||||
let outputs = f.evaluate(r#""kick" s ( . ) 4 2 except+"#, &ctx).unwrap();
|
||||
let outputs = f.evaluate(r#""kick" snd ( . ) 4 2 except+"#, &ctx).unwrap();
|
||||
if iter % 4 != 2 {
|
||||
assert_eq!(outputs.len(), 1, "iter={}: should fire", iter);
|
||||
} else {
|
||||
@@ -389,8 +389,8 @@ fn every_offset_zero_is_same_as_every() {
|
||||
for iter in 0..8 {
|
||||
let ctx = ctx_with(|c| c.iter = iter);
|
||||
let f = forth();
|
||||
let a = f.evaluate(r#""kick" s ( . ) 3 every"#, &ctx).unwrap();
|
||||
let b = f.evaluate(r#""kick" s ( . ) 3 0 every+"#, &ctx).unwrap();
|
||||
let a = f.evaluate(r#""kick" snd ( . ) 3 every"#, &ctx).unwrap();
|
||||
let b = f.evaluate(r#""kick" snd ( . ) 3 0 every+"#, &ctx).unwrap();
|
||||
assert_eq!(a.len(), b.len(), "iter={}: every and every+ 0 should match", iter);
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user