Feat: lissajous
Some checks failed
Deploy Website / deploy (push) Failing after 4m50s

This commit is contained in:
2026-02-23 22:06:09 +01:00
parent 77b7fa1f9e
commit 4a8396670f
17 changed files with 241 additions and 49 deletions

View File

@@ -5,6 +5,7 @@ mod confirm;
mod editor;
mod file_browser;
mod hint_bar;
mod lissajous;
mod list_select;
mod modal;
mod nav_minimap;
@@ -26,6 +27,7 @@ pub use confirm::ConfirmModal;
pub use editor::{fuzzy_match, CompletionCandidate, Editor};
pub use file_browser::FileBrowserModal;
pub use hint_bar::hint_line;
pub use lissajous::Lissajous;
pub use list_select::ListSelect;
pub use modal::ModalFrame;
pub use nav_minimap::{hit_test_tile, minimap_area, NavMinimap, NavTile};

View File

@@ -0,0 +1,103 @@
use crate::theme;
use ratatui::buffer::Buffer;
use ratatui::layout::Rect;
use ratatui::style::Color;
use ratatui::widgets::Widget;
use std::cell::RefCell;
thread_local! {
static PATTERNS: RefCell<Vec<u8>> = const { RefCell::new(Vec::new()) };
}
pub struct Lissajous<'a> {
left: &'a [f32],
right: &'a [f32],
color: Option<Color>,
}
impl<'a> Lissajous<'a> {
pub fn new(left: &'a [f32], right: &'a [f32]) -> Self {
Self {
left,
right,
color: None,
}
}
pub fn color(mut self, c: Color) -> Self {
self.color = Some(c);
self
}
}
impl Widget for Lissajous<'_> {
fn render(self, area: Rect, buf: &mut Buffer) {
if area.width == 0 || area.height == 0 || self.left.is_empty() || self.right.is_empty() {
return;
}
let color = self.color.unwrap_or_else(|| theme::get().meter.low);
let width = area.width as usize;
let height = area.height as usize;
let fine_width = width * 2;
let fine_height = height * 4;
let len = self.left.len().min(self.right.len());
let peak = self
.left
.iter()
.chain(self.right.iter())
.map(|s| s.abs())
.fold(0.0f32, f32::max);
let gain = if peak > 0.001 { 1.0 / peak } else { 1.0 };
PATTERNS.with(|p| {
let mut patterns = p.borrow_mut();
let size = width * height;
patterns.clear();
patterns.resize(size, 0);
for i in 0..len {
let l = (self.left[i] * gain).clamp(-1.0, 1.0);
let r = (self.right[i] * gain).clamp(-1.0, 1.0);
// X = right channel, Y = left channel (inverted so up = positive)
let fine_x = ((r + 1.0) * 0.5 * (fine_width - 1) as f32).round() as usize;
let fine_y = ((1.0 - l) * 0.5 * (fine_height - 1) as f32).round() as usize;
let fine_x = fine_x.min(fine_width - 1);
let fine_y = fine_y.min(fine_height - 1);
let char_x = fine_x / 2;
let char_y = fine_y / 4;
let dot_x = fine_x % 2;
let dot_y = fine_y % 4;
let bit = match (dot_x, dot_y) {
(0, 0) => 0x01,
(0, 1) => 0x02,
(0, 2) => 0x04,
(0, 3) => 0x40,
(1, 0) => 0x08,
(1, 1) => 0x10,
(1, 2) => 0x20,
(1, 3) => 0x80,
_ => unreachable!(),
};
patterns[char_y * width + char_x] |= bit;
}
for cy in 0..height {
for cx in 0..width {
let pattern = patterns[cy * width + cx];
if pattern != 0 {
let ch = char::from_u32(0x2800 + pattern as u32).unwrap_or(' ');
buf[(area.x + cx as u16, area.y + cy as u16)]
.set_char(ch)
.set_fg(color);
}
}
}
});
}
}

View File

@@ -389,6 +389,7 @@ impl App {
AppCommand::ToggleRefreshRate => self.audio.toggle_refresh_rate(),
AppCommand::ToggleScope => self.audio.config.show_scope = !self.audio.config.show_scope,
AppCommand::ToggleSpectrum => self.audio.config.show_spectrum = !self.audio.config.show_spectrum,
AppCommand::ToggleLissajous => self.audio.config.show_lissajous = !self.audio.config.show_lissajous,
AppCommand::TogglePreview => self.audio.config.show_preview = !self.audio.config.show_preview,
AppCommand::TogglePerformanceMode => self.ui.performance_mode = !self.ui.performance_mode,

View File

@@ -24,6 +24,7 @@ impl App {
runtime_highlight: self.ui.runtime_highlight,
show_scope: self.audio.config.show_scope,
show_spectrum: self.audio.config.show_spectrum,
show_lissajous: self.audio.config.show_lissajous,
show_preview: self.audio.config.show_preview,
show_completion: self.ui.show_completion,
performance_mode: self.ui.performance_mode,

View File

@@ -307,6 +307,7 @@ impl CagireDesktop {
self.app.metrics.schedule_depth =
self.metrics.schedule_depth.load(Ordering::Relaxed) as usize;
self.app.metrics.scope = self.scope_buffer.read();
self.app.metrics.scope_right = self.scope_buffer.read_right();
(self.app.metrics.peak_left, self.app.metrics.peak_right) = self.scope_buffer.peaks();
self.app.metrics.spectrum = self.spectrum_buffer.read();
self.app.metrics.nudge_ms = self.nudge_us.load(Ordering::Relaxed) as f64 / 1000.0;

View File

@@ -253,6 +253,7 @@ pub enum AppCommand {
ToggleRefreshRate,
ToggleScope,
ToggleSpectrum,
ToggleLissajous,
TogglePreview,
TogglePerformanceMode,

View File

@@ -11,6 +11,7 @@ use std::sync::atomic::AtomicU64;
pub struct ScopeBuffer {
pub samples: [AtomicU32; 256],
pub samples_right: [AtomicU32; 256],
peak_left: AtomicU32,
peak_right: AtomicU32,
}
@@ -25,6 +26,7 @@ impl ScopeBuffer {
pub fn new() -> Self {
Self {
samples: std::array::from_fn(|_| AtomicU32::new(0)),
samples_right: std::array::from_fn(|_| AtomicU32::new(0)),
peak_left: AtomicU32::new(0),
peak_right: AtomicU32::new(0),
}
@@ -44,10 +46,14 @@ impl ScopeBuffer {
// Downsample for scope display
let frames = data.len() / 2;
for (i, atom) in self.samples.iter().enumerate() {
for (i, (left_atom, right_atom)) in
self.samples.iter().zip(self.samples_right.iter()).enumerate()
{
let frame_idx = (i * frames) / self.samples.len();
let left = data.get(frame_idx * 2).copied().unwrap_or(0.0);
atom.store(left.to_bits(), Ordering::Relaxed);
let right = data.get(frame_idx * 2 + 1).copied().unwrap_or(0.0);
left_atom.store(left.to_bits(), Ordering::Relaxed);
right_atom.store(right.to_bits(), Ordering::Relaxed);
}
self.peak_left.store(peak_l.to_bits(), Ordering::Relaxed);
@@ -58,6 +64,10 @@ impl ScopeBuffer {
std::array::from_fn(|i| f32::from_bits(self.samples[i].load(Ordering::Relaxed)))
}
pub fn read_right(&self) -> [f32; 256] {
std::array::from_fn(|i| f32::from_bits(self.samples_right[i].load(Ordering::Relaxed)))
}
pub fn peaks(&self) -> (f32, f32) {
let left = f32::from_bits(self.peak_left.load(Ordering::Relaxed));
let right = f32::from_bits(self.peak_right.load(Ordering::Relaxed));

View File

@@ -109,6 +109,7 @@ pub fn init(args: InitArgs) -> Init {
app.ui.runtime_highlight = settings.display.runtime_highlight;
app.audio.config.show_scope = settings.display.show_scope;
app.audio.config.show_spectrum = settings.display.show_spectrum;
app.audio.config.show_lissajous = settings.display.show_lissajous;
app.audio.config.show_preview = settings.display.show_preview;
app.ui.show_completion = settings.display.show_completion;
app.ui.performance_mode = settings.display.performance_mode;

View File

@@ -24,6 +24,7 @@ pub(crate) fn cycle_option_value(ctx: &mut InputContext, right: bool) {
OptionsFocus::RuntimeHighlight => ctx.dispatch(AppCommand::ToggleRuntimeHighlight),
OptionsFocus::ShowScope => ctx.dispatch(AppCommand::ToggleScope),
OptionsFocus::ShowSpectrum => ctx.dispatch(AppCommand::ToggleSpectrum),
OptionsFocus::ShowLissajous => ctx.dispatch(AppCommand::ToggleLissajous),
OptionsFocus::ShowCompletion => ctx.dispatch(AppCommand::ToggleCompletion),
OptionsFocus::ShowPreview => ctx.dispatch(AppCommand::TogglePreview),
OptionsFocus::PerformanceMode => ctx.dispatch(AppCommand::TogglePerformanceMode),

View File

@@ -231,6 +231,7 @@ fn main() -> io::Result<()> {
app.metrics.cpu_load = metrics.load.get_load();
app.metrics.schedule_depth = metrics.schedule_depth.load(Ordering::Relaxed) as usize;
app.metrics.scope = scope_buffer.read();
app.metrics.scope_right = scope_buffer.read_right();
(app.metrics.peak_left, app.metrics.peak_right) = scope_buffer.peaks();
app.metrics.spectrum = spectrum_buffer.read();
app.metrics.nudge_ms = nudge_us.load(Ordering::Relaxed) as f64 / 1000.0;

View File

@@ -41,6 +41,8 @@ pub struct DisplaySettings {
pub show_scope: bool,
pub show_spectrum: bool,
#[serde(default = "default_true")]
pub show_lissajous: bool,
#[serde(default = "default_true")]
pub show_preview: bool,
#[serde(default = "default_true")]
pub show_completion: bool,
@@ -100,6 +102,7 @@ impl Default for DisplaySettings {
runtime_highlight: false,
show_scope: true,
show_spectrum: true,
show_lissajous: true,
show_preview: true,
show_completion: true,
font: default_font(),

View File

@@ -83,6 +83,7 @@ pub struct AudioConfig {
pub refresh_rate: RefreshRate,
pub show_scope: bool,
pub show_spectrum: bool,
pub show_lissajous: bool,
pub show_preview: bool,
pub layout: MainLayout,
}
@@ -102,6 +103,7 @@ impl Default for AudioConfig {
refresh_rate: RefreshRate::default(),
show_scope: true,
show_spectrum: true,
show_lissajous: true,
show_preview: true,
layout: MainLayout::default(),
}
@@ -191,6 +193,7 @@ pub struct Metrics {
pub cpu_load: f32,
pub schedule_depth: usize,
pub scope: [f32; 256],
pub scope_right: [f32; 256],
pub peak_left: f32,
pub peak_right: f32,
pub spectrum: [f32; 32],
@@ -206,6 +209,7 @@ impl Default for Metrics {
cpu_load: 0.0,
schedule_depth: 0,
scope: [0.0; 256],
scope_right: [0.0; 256],
peak_left: 0.0,
peak_right: 0.0,
spectrum: [0.0; 32],

View File

@@ -9,6 +9,7 @@ pub enum OptionsFocus {
RuntimeHighlight,
ShowScope,
ShowSpectrum,
ShowLissajous,
ShowCompletion,
ShowPreview,
PerformanceMode,
@@ -38,6 +39,7 @@ impl CyclicEnum for OptionsFocus {
Self::RuntimeHighlight,
Self::ShowScope,
Self::ShowSpectrum,
Self::ShowLissajous,
Self::ShowCompletion,
Self::ShowPreview,
Self::PerformanceMode,
@@ -93,30 +95,31 @@ const FULL_LAYOUT: &[(OptionsFocus, usize)] = &[
(OptionsFocus::RuntimeHighlight, 5),
(OptionsFocus::ShowScope, 6),
(OptionsFocus::ShowSpectrum, 7),
(OptionsFocus::ShowCompletion, 8),
(OptionsFocus::ShowPreview, 9),
(OptionsFocus::PerformanceMode, 10),
(OptionsFocus::Font, 11),
(OptionsFocus::ZoomFactor, 12),
(OptionsFocus::WindowSize, 13),
// blank=14, ABLETON LINK header=15, divider=16
(OptionsFocus::LinkEnabled, 17),
(OptionsFocus::StartStopSync, 18),
(OptionsFocus::Quantum, 19),
// blank=20, SESSION header=21, divider=22, Tempo=23, Beat=24, Phase=25
// blank=26, MIDI OUTPUTS header=27, divider=28
(OptionsFocus::MidiOutput0, 29),
(OptionsFocus::MidiOutput1, 30),
(OptionsFocus::MidiOutput2, 31),
(OptionsFocus::MidiOutput3, 32),
// blank=33, MIDI INPUTS header=34, divider=35
(OptionsFocus::MidiInput0, 36),
(OptionsFocus::MidiInput1, 37),
(OptionsFocus::MidiInput2, 38),
(OptionsFocus::MidiInput3, 39),
// blank=40, ONBOARDING header=41, divider=42
(OptionsFocus::ResetOnboarding, 43),
(OptionsFocus::LoadDemoOnStartup, 44),
(OptionsFocus::ShowLissajous, 8),
(OptionsFocus::ShowCompletion, 9),
(OptionsFocus::ShowPreview, 10),
(OptionsFocus::PerformanceMode, 11),
(OptionsFocus::Font, 12),
(OptionsFocus::ZoomFactor, 13),
(OptionsFocus::WindowSize, 14),
// blank=15, ABLETON LINK header=16, divider=17
(OptionsFocus::LinkEnabled, 18),
(OptionsFocus::StartStopSync, 19),
(OptionsFocus::Quantum, 20),
// blank=21, SESSION header=22, divider=23, Tempo=24, Beat=25, Phase=26
// blank=27, MIDI OUTPUTS header=28, divider=29
(OptionsFocus::MidiOutput0, 30),
(OptionsFocus::MidiOutput1, 31),
(OptionsFocus::MidiOutput2, 32),
(OptionsFocus::MidiOutput3, 33),
// blank=34, MIDI INPUTS header=35, divider=36
(OptionsFocus::MidiInput0, 37),
(OptionsFocus::MidiInput1, 38),
(OptionsFocus::MidiInput2, 39),
(OptionsFocus::MidiInput3, 40),
// blank=41, ONBOARDING header=42, divider=43
(OptionsFocus::ResetOnboarding, 44),
(OptionsFocus::LoadDemoOnStartup, 45),
];
impl OptionsFocus {
@@ -172,13 +175,13 @@ fn visible_layout(plugin_mode: bool) -> Vec<(OptionsFocus, usize)> {
// based on which sections are hidden.
let mut offset: usize = 0;
// Font/Zoom/Window lines (11,12,13) hidden when !plugin_mode
// Font/Zoom/Window lines (12,13,14) hidden when !plugin_mode
if !plugin_mode {
offset += 3; // 3 lines for Font, ZoomFactor, WindowSize
}
// Link + Session + MIDI sections hidden when plugin_mode
// These span from blank(14) through MidiInput3(39) = 26 lines
// These span from blank(15) through MidiInput3(40) = 26 lines
if plugin_mode {
let link_section_lines = 26;
offset += link_section_lines;
@@ -189,10 +192,10 @@ fn visible_layout(plugin_mode: bool) -> Vec<(OptionsFocus, usize)> {
if !focus.is_visible(plugin_mode) {
continue;
}
// Lines at or below index 10 (PerformanceMode) are never shifted
let adjusted = if raw_line <= 10 {
// Lines at or below index 11 (PerformanceMode) are never shifted
let adjusted = if raw_line <= 11 {
raw_line
} else if !plugin_mode && raw_line <= 13 {
} else if !plugin_mode && raw_line <= 14 {
// Font/Zoom/Window — these are hidden, skip
continue;
} else {

View File

@@ -9,7 +9,8 @@ use crate::app::App;
use crate::state::{DeviceKind, EngineSection, SettingKind};
use crate::theme;
use crate::widgets::{
render_scroll_indicators, render_section_header, IndicatorAlign, Orientation, Scope, Spectrum,
render_scroll_indicators, render_section_header, IndicatorAlign, Lissajous, Orientation, Scope,
Spectrum,
};
pub fn layout(area: Rect) -> [Rect; 3] {
@@ -148,14 +149,17 @@ fn render_settings_section(frame: &mut Frame, app: &App, area: Rect) {
}
fn render_visualizers(frame: &mut Frame, app: &App, area: Rect) {
let [scope_area, _, spectrum_area] = Layout::vertical([
Constraint::Percentage(50),
let [scope_area, _, lissajous_area, _gap, spectrum_area] = Layout::vertical([
Constraint::Fill(1),
Constraint::Length(1),
Constraint::Percentage(50),
Constraint::Fill(1),
Constraint::Length(1),
Constraint::Fill(1),
])
.areas(area);
render_scope(frame, app, scope_area);
render_lissajous(frame, app, lissajous_area);
render_spectrum(frame, app, spectrum_area);
}
@@ -175,6 +179,21 @@ fn render_scope(frame: &mut Frame, app: &App, area: Rect) {
frame.render_widget(scope, inner);
}
fn render_lissajous(frame: &mut Frame, app: &App, area: Rect) {
let theme = theme::get();
let block = Block::default()
.borders(Borders::ALL)
.title(" Lissajous ")
.border_style(Style::new().fg(theme.engine.border_green));
let inner = block.inner(area);
frame.render_widget(block, area);
let lissajous = Lissajous::new(&app.metrics.scope, &app.metrics.scope_right)
.color(theme.meter.low);
frame.render_widget(lissajous, inner);
}
fn render_spectrum(frame: &mut Frame, app: &App, area: Rect) {
let theme = theme::get();
let block = Block::default()

View File

@@ -12,7 +12,7 @@ use crate::engine::SequencerSnapshot;
use crate::state::MainLayout;
use crate::theme;
use crate::views::render::highlight_script_lines;
use crate::widgets::{Orientation, Scope, Spectrum, VuMeter};
use crate::widgets::{Lissajous, Orientation, Scope, Spectrum, VuMeter};
pub fn layout(area: Rect) -> [Rect; 3] {
Layout::horizontal([
@@ -31,6 +31,7 @@ pub fn render(frame: &mut Frame, app: &App, snapshot: &SequencerSnapshot, area:
} else {
let has_viz = app.audio.config.show_scope
|| app.audio.config.show_spectrum
|| app.audio.config.show_lissajous
|| app.audio.config.show_preview;
let (viz_area, sequencer_area) =
viz_seq_split(main_area, app.audio.config.layout, has_viz);
@@ -49,7 +50,9 @@ fn render_top_layout(
snapshot: &SequencerSnapshot,
main_area: Rect,
) {
let has_audio_viz = app.audio.config.show_scope || app.audio.config.show_spectrum;
let has_audio_viz = app.audio.config.show_scope
|| app.audio.config.show_spectrum
|| app.audio.config.show_lissajous;
let has_preview = app.audio.config.show_preview;
let mut constraints = Vec::new();
@@ -85,16 +88,23 @@ fn render_top_layout(
}
fn render_audio_viz(frame: &mut Frame, app: &App, area: Rect) {
match (app.audio.config.show_scope, app.audio.config.show_spectrum) {
(true, true) => {
let [scope_area, spectrum_area] =
Layout::horizontal([Constraint::Fill(1), Constraint::Fill(1)]).areas(area);
render_scope(frame, app, scope_area, Orientation::Horizontal);
render_spectrum(frame, app, spectrum_area);
let mut panels: Vec<VizPanel> = Vec::new();
if app.audio.config.show_scope { panels.push(VizPanel::Scope); }
if app.audio.config.show_spectrum { panels.push(VizPanel::Spectrum); }
if app.audio.config.show_lissajous { panels.push(VizPanel::Lissajous); }
if panels.is_empty() { return; }
let constraints: Vec<Constraint> = panels.iter().map(|_| Constraint::Fill(1)).collect();
let areas: Vec<Rect> = Layout::horizontal(&constraints).split(area).to_vec();
for (panel, panel_area) in panels.iter().zip(areas.iter()) {
match panel {
VizPanel::Scope => render_scope(frame, app, *panel_area, Orientation::Horizontal),
VizPanel::Spectrum => render_spectrum(frame, app, *panel_area),
VizPanel::Lissajous => render_lissajous(frame, app, *panel_area),
VizPanel::Preview => {}
}
(true, false) => render_scope(frame, app, area, Orientation::Horizontal),
(false, true) => render_spectrum(frame, app, area),
(false, false) => {}
}
}
@@ -104,7 +114,9 @@ fn preview_height(has_audio_viz: bool) -> u16 {
pub fn sequencer_rect(app: &App, main_area: Rect) -> Rect {
if matches!(app.audio.config.layout, MainLayout::Top) {
let has_audio_viz = app.audio.config.show_scope || app.audio.config.show_spectrum;
let has_audio_viz = app.audio.config.show_scope
|| app.audio.config.show_spectrum
|| app.audio.config.show_lissajous;
let has_preview = app.audio.config.show_preview;
let mut constraints = Vec::new();
@@ -121,6 +133,7 @@ pub fn sequencer_rect(app: &App, main_area: Rect) -> Rect {
} else {
let has_viz = app.audio.config.show_scope
|| app.audio.config.show_spectrum
|| app.audio.config.show_lissajous
|| app.audio.config.show_preview;
let (_, seq_area) = viz_seq_split(main_area, app.audio.config.layout, has_viz);
seq_area
@@ -130,6 +143,7 @@ pub fn sequencer_rect(app: &App, main_area: Rect) -> Rect {
enum VizPanel {
Scope,
Spectrum,
Lissajous,
Preview,
}
@@ -142,11 +156,13 @@ fn render_viz_area(
let is_vertical_layout = matches!(app.audio.config.layout, MainLayout::Left | MainLayout::Right);
let show_scope = app.audio.config.show_scope;
let show_spectrum = app.audio.config.show_spectrum;
let show_lissajous = app.audio.config.show_lissajous;
let show_preview = app.audio.config.show_preview;
let mut panels = Vec::new();
if show_scope { panels.push(VizPanel::Scope); }
if show_spectrum { panels.push(VizPanel::Spectrum); }
if show_lissajous { panels.push(VizPanel::Lissajous); }
if show_preview { panels.push(VizPanel::Preview); }
let constraints: Vec<Constraint> = panels.iter().map(|_| Constraint::Fill(1)).collect();
@@ -173,6 +189,7 @@ fn render_viz_area(
match panel {
VizPanel::Scope => render_scope(frame, app, *panel_area, orientation),
VizPanel::Spectrum => render_spectrum(frame, app, *panel_area),
VizPanel::Lissajous => render_lissajous(frame, app, *panel_area),
VizPanel::Preview => {
let user_words = user_words_once.as_ref().unwrap();
let has_prelude = !app.project_state.project.prelude.trim().is_empty();
@@ -491,6 +508,19 @@ fn render_spectrum(frame: &mut Frame, app: &App, area: Rect) {
frame.render_widget(spectrum, inner);
}
fn render_lissajous(frame: &mut Frame, app: &App, area: Rect) {
let theme = theme::get();
let block = Block::default()
.borders(Borders::ALL)
.border_style(Style::new().fg(theme.ui.border));
let inner = block.inner(area);
frame.render_widget(block, area);
let lissajous = Lissajous::new(&app.metrics.scope, &app.metrics.scope_right)
.color(theme.meter.low);
frame.render_widget(lissajous, inner);
}
fn render_script_preview(
frame: &mut Frame,
app: &App,

View File

@@ -78,6 +78,16 @@ pub fn render(frame: &mut Frame, app: &App, link: &LinkState, area: Rect) {
focus == OptionsFocus::ShowSpectrum,
&theme,
),
render_option_line(
"Show lissajous",
if app.audio.config.show_lissajous {
"On"
} else {
"Off"
},
focus == OptionsFocus::ShowLissajous,
&theme,
),
render_option_line(
"Completion",
if app.ui.show_completion { "On" } else { "Off" },
@@ -346,6 +356,7 @@ fn option_description(focus: OptionsFocus) -> Option<&'static str> {
OptionsFocus::RuntimeHighlight => Some("Highlight executed code spans during playback"),
OptionsFocus::ShowScope => Some("Oscilloscope on the engine page"),
OptionsFocus::ShowSpectrum => Some("Spectrum analyzer on the engine page"),
OptionsFocus::ShowLissajous => Some("XY stereo phase scope (left vs right)"),
OptionsFocus::ShowCompletion => Some("Word completion popup in the editor"),
OptionsFocus::ShowPreview => Some("Step script preview on the sequencer grid"),
OptionsFocus::PerformanceMode => Some("Hide header and footer bars"),

View File

@@ -1,6 +1,6 @@
pub use cagire_ratatui::{
hint_line, render_props_form, render_scroll_indicators, render_search_bar,
render_section_header, CategoryItem, CategoryList, ConfirmModal, FileBrowserModal,
IndicatorAlign, ModalFrame, NavMinimap, NavTile, Orientation, SampleBrowser, Scope, Selection,
Spectrum, TextInputModal, VuMeter, Waveform,
IndicatorAlign, Lissajous, ModalFrame, NavMinimap, NavTile, Orientation, SampleBrowser, Scope,
Selection, Spectrum, TextInputModal, VuMeter, Waveform,
};