WIP: rename to cagire-plugins
This commit is contained in:
273
crates/plugins/src/editor.rs
Normal file
273
crates/plugins/src/editor.rs
Normal file
@@ -0,0 +1,273 @@
|
||||
use std::sync::atomic::{AtomicBool, AtomicI64};
|
||||
use std::sync::Arc;
|
||||
use std::time::Instant;
|
||||
|
||||
use arc_swap::ArcSwap;
|
||||
use crossbeam_channel::Sender;
|
||||
use egui_ratatui::RataguiBackend;
|
||||
use nih_plug::prelude::*;
|
||||
use nih_plug_egui::egui;
|
||||
use nih_plug_egui::{create_egui_editor, EguiState};
|
||||
use ratatui::Terminal;
|
||||
use soft_ratatui::embedded_graphics_unicodefonts::{
|
||||
mono_10x20_atlas, mono_6x13_atlas, mono_6x13_bold_atlas, mono_6x13_italic_atlas,
|
||||
mono_7x13_atlas, mono_7x13_bold_atlas, mono_7x13_italic_atlas, mono_8x13_atlas,
|
||||
mono_8x13_bold_atlas, mono_8x13_italic_atlas, mono_9x15_atlas, mono_9x15_bold_atlas,
|
||||
mono_9x18_atlas, mono_9x18_bold_atlas,
|
||||
};
|
||||
use soft_ratatui::{EmbeddedGraphics, SoftBackend};
|
||||
|
||||
use cagire::app::App;
|
||||
use cagire::engine::{AudioCommand, LinkState, SequencerSnapshot};
|
||||
use cagire::input::{handle_key, handle_mouse, InputContext};
|
||||
use cagire::model::{Dictionary, Rng, Variables};
|
||||
use cagire::theme;
|
||||
use cagire::views;
|
||||
|
||||
use crate::input_egui::{convert_egui_events, convert_egui_mouse};
|
||||
use crate::params::CagireParams;
|
||||
use crate::PluginBridge;
|
||||
|
||||
type TerminalType = Terminal<RataguiBackend<EmbeddedGraphics>>;
|
||||
|
||||
#[derive(Clone, Copy, PartialEq)]
|
||||
enum FontChoice {
|
||||
Size6x13,
|
||||
Size7x13,
|
||||
Size8x13,
|
||||
Size9x15,
|
||||
Size9x18,
|
||||
Size10x20,
|
||||
}
|
||||
|
||||
impl FontChoice {
|
||||
fn label(self) -> &'static str {
|
||||
match self {
|
||||
Self::Size6x13 => "6x13 (Compact)",
|
||||
Self::Size7x13 => "7x13",
|
||||
Self::Size8x13 => "8x13 (Default)",
|
||||
Self::Size9x15 => "9x15",
|
||||
Self::Size9x18 => "9x18",
|
||||
Self::Size10x20 => "10x20 (Large)",
|
||||
}
|
||||
}
|
||||
|
||||
const ALL: [Self; 6] = [
|
||||
Self::Size6x13,
|
||||
Self::Size7x13,
|
||||
Self::Size8x13,
|
||||
Self::Size9x15,
|
||||
Self::Size9x18,
|
||||
Self::Size10x20,
|
||||
];
|
||||
}
|
||||
|
||||
fn create_terminal(font: FontChoice) -> TerminalType {
|
||||
let (regular, bold, italic) = match font {
|
||||
FontChoice::Size6x13 => (
|
||||
mono_6x13_atlas(),
|
||||
Some(mono_6x13_bold_atlas()),
|
||||
Some(mono_6x13_italic_atlas()),
|
||||
),
|
||||
FontChoice::Size7x13 => (
|
||||
mono_7x13_atlas(),
|
||||
Some(mono_7x13_bold_atlas()),
|
||||
Some(mono_7x13_italic_atlas()),
|
||||
),
|
||||
FontChoice::Size8x13 => (
|
||||
mono_8x13_atlas(),
|
||||
Some(mono_8x13_bold_atlas()),
|
||||
Some(mono_8x13_italic_atlas()),
|
||||
),
|
||||
FontChoice::Size9x15 => (mono_9x15_atlas(), Some(mono_9x15_bold_atlas()), None),
|
||||
FontChoice::Size9x18 => (mono_9x18_atlas(), Some(mono_9x18_bold_atlas()), None),
|
||||
FontChoice::Size10x20 => (mono_10x20_atlas(), None, None),
|
||||
};
|
||||
|
||||
let soft = SoftBackend::<EmbeddedGraphics>::new(80, 24, regular, bold, italic);
|
||||
Terminal::new(RataguiBackend::new("cagire", soft)).expect("terminal")
|
||||
}
|
||||
|
||||
struct EditorState {
|
||||
app: App,
|
||||
terminal: TerminalType,
|
||||
link: Arc<LinkState>,
|
||||
snapshot: SequencerSnapshot,
|
||||
playing: Arc<AtomicBool>,
|
||||
nudge_us: Arc<AtomicI64>,
|
||||
audio_tx: Arc<ArcSwap<Sender<AudioCommand>>>,
|
||||
bridge: Arc<PluginBridge>,
|
||||
params: Arc<CagireParams>,
|
||||
last_frame: Instant,
|
||||
current_font: FontChoice,
|
||||
zoom_factor: f32,
|
||||
}
|
||||
|
||||
// SAFETY: EditorState is only accessed from the GUI thread via RwLock.
|
||||
// The non-Send types (RefCell in App's tachyonfx effects) are never shared
|
||||
// across threads — baseview's event loop guarantees single-threaded access.
|
||||
unsafe impl Send for EditorState {}
|
||||
unsafe impl Sync for EditorState {}
|
||||
|
||||
pub fn create_editor(
|
||||
params: Arc<CagireParams>,
|
||||
egui_state: Arc<EguiState>,
|
||||
bridge: Arc<PluginBridge>,
|
||||
variables: Variables,
|
||||
dict: Dictionary,
|
||||
rng: Rng,
|
||||
) -> Option<Box<dyn Editor>> {
|
||||
create_egui_editor(
|
||||
egui_state,
|
||||
None::<EditorState>,
|
||||
|ctx, _state| {
|
||||
ctx.set_visuals(egui::Visuals {
|
||||
panel_fill: egui::Color32::BLACK,
|
||||
..egui::Visuals::dark()
|
||||
});
|
||||
},
|
||||
move |ctx, _setter, state| {
|
||||
let editor = state.get_or_insert_with(|| {
|
||||
let palette =
|
||||
cagire::state::ColorScheme::default().to_palette();
|
||||
theme::set(cagire_ratatui::theme::build::build(&palette));
|
||||
|
||||
let mut app = App::new_plugin(
|
||||
Arc::clone(&variables),
|
||||
Arc::clone(&dict),
|
||||
Arc::clone(&rng),
|
||||
);
|
||||
app.audio.section = cagire::state::EngineSection::Settings;
|
||||
app.audio.setting_kind = cagire::state::SettingKind::Polyphony;
|
||||
|
||||
// Load persisted project
|
||||
app.project_state.project = params.project.lock().clone();
|
||||
app.mark_all_patterns_dirty();
|
||||
|
||||
EditorState {
|
||||
app,
|
||||
terminal: create_terminal(FontChoice::Size8x13),
|
||||
link: Arc::new(LinkState::new(
|
||||
params.tempo.value() as f64,
|
||||
4.0,
|
||||
)),
|
||||
snapshot: SequencerSnapshot::empty(),
|
||||
playing: Arc::new(AtomicBool::new(false)),
|
||||
nudge_us: Arc::new(AtomicI64::new(0)),
|
||||
audio_tx: Arc::new(ArcSwap::from_pointee(bridge.audio_cmd_tx.clone())),
|
||||
bridge: Arc::clone(&bridge),
|
||||
params: Arc::clone(¶ms),
|
||||
last_frame: Instant::now(),
|
||||
current_font: FontChoice::Size8x13,
|
||||
zoom_factor: 1.0,
|
||||
}
|
||||
});
|
||||
|
||||
// Flush pattern data and queued changes to the audio thread
|
||||
let had_dirty = editor.app.flush_dirty_patterns(&editor.bridge.cmd_tx);
|
||||
editor.app.flush_queued_changes(&editor.bridge.cmd_tx);
|
||||
|
||||
// Sync project changes back to persisted params
|
||||
if had_dirty {
|
||||
*editor.params.project.lock() = editor.app.project_state.project.clone();
|
||||
}
|
||||
|
||||
// Sync sample registry from the audio engine (set once after initialize)
|
||||
if editor.app.audio.sample_registry.is_none() {
|
||||
if let Some(reg) = editor.bridge.sample_registry.load().as_ref() {
|
||||
editor.app.audio.sample_registry = Some(Arc::clone(reg));
|
||||
}
|
||||
}
|
||||
|
||||
// Read live snapshot from the audio thread
|
||||
let shared = editor.bridge.shared_state.load();
|
||||
editor.snapshot = SequencerSnapshot::from(shared.as_ref());
|
||||
|
||||
// Feed scope and spectrum data into app metrics
|
||||
editor.app.metrics.scope = editor.bridge.scope_buffer.read();
|
||||
(editor.app.metrics.peak_left, editor.app.metrics.peak_right) =
|
||||
editor.bridge.scope_buffer.peaks();
|
||||
editor.app.metrics.spectrum = editor.bridge.spectrum_buffer.read();
|
||||
|
||||
// Handle input
|
||||
let term = editor.terminal.get_frame().area();
|
||||
let widget_rect = ctx.content_rect();
|
||||
|
||||
for mouse in convert_egui_mouse(ctx, widget_rect, term) {
|
||||
let mut input_ctx = InputContext {
|
||||
app: &mut editor.app,
|
||||
link: &editor.link,
|
||||
snapshot: &editor.snapshot,
|
||||
playing: &editor.playing,
|
||||
audio_tx: &editor.audio_tx,
|
||||
seq_cmd_tx: &editor.bridge.cmd_tx,
|
||||
nudge_us: &editor.nudge_us,
|
||||
};
|
||||
handle_mouse(&mut input_ctx, mouse, term);
|
||||
}
|
||||
|
||||
for key in convert_egui_events(ctx) {
|
||||
let mut input_ctx = InputContext {
|
||||
app: &mut editor.app,
|
||||
link: &editor.link,
|
||||
snapshot: &editor.snapshot,
|
||||
playing: &editor.playing,
|
||||
audio_tx: &editor.audio_tx,
|
||||
seq_cmd_tx: &editor.bridge.cmd_tx,
|
||||
nudge_us: &editor.nudge_us,
|
||||
};
|
||||
let _ = handle_key(&mut input_ctx, key);
|
||||
}
|
||||
|
||||
cagire::state::effects::tick_effects(&mut editor.app.ui, editor.app.page);
|
||||
|
||||
let elapsed = editor.last_frame.elapsed();
|
||||
editor.last_frame = Instant::now();
|
||||
|
||||
let link = &editor.link;
|
||||
let app = &editor.app;
|
||||
let snapshot = &editor.snapshot;
|
||||
editor
|
||||
.terminal
|
||||
.draw(|frame| views::render(frame, app, link, snapshot, elapsed))
|
||||
.expect("draw");
|
||||
|
||||
let current_font = editor.current_font;
|
||||
let current_zoom = editor.zoom_factor;
|
||||
|
||||
egui::CentralPanel::default()
|
||||
.frame(egui::Frame::NONE.fill(egui::Color32::BLACK))
|
||||
.show(ctx, |ui| {
|
||||
ui.add(editor.terminal.backend_mut());
|
||||
|
||||
let response = ui.interact(
|
||||
ui.max_rect(),
|
||||
egui::Id::new("terminal_context"),
|
||||
egui::Sense::click(),
|
||||
);
|
||||
response.context_menu(|ui| {
|
||||
ui.menu_button("Font", |ui| {
|
||||
for choice in FontChoice::ALL {
|
||||
if ui.selectable_label(current_font == choice, choice.label()).clicked() {
|
||||
editor.terminal = create_terminal(choice);
|
||||
editor.current_font = choice;
|
||||
ui.close();
|
||||
}
|
||||
}
|
||||
});
|
||||
ui.menu_button("Zoom", |ui| {
|
||||
for &level in &[0.5_f32, 0.75, 1.0, 1.25, 1.5, 1.75, 2.0] {
|
||||
let selected = (current_zoom - level).abs() < 0.01;
|
||||
let label = format!("{:.0}%", level * 100.0);
|
||||
if ui.selectable_label(selected, label).clicked() {
|
||||
editor.zoom_factor = level;
|
||||
ctx.set_zoom_factor(level);
|
||||
ui.close();
|
||||
}
|
||||
}
|
||||
});
|
||||
});
|
||||
});
|
||||
},
|
||||
)
|
||||
}
|
||||
Reference in New Issue
Block a user