Feat: saving screen during perfs

This commit is contained in:
2026-02-21 15:56:52 +01:00
parent 12b90bc99b
commit 79a4c3b6e2
17 changed files with 117 additions and 223 deletions

View File

@@ -388,6 +388,7 @@ impl App {
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::TogglePreview => self.audio.config.show_preview = !self.audio.config.show_preview,
AppCommand::TogglePerformanceMode => self.ui.performance_mode = !self.ui.performance_mode,
// Metrics
AppCommand::ResetPeakVoices => self.metrics.peak_voices = 0,

View File

@@ -26,6 +26,7 @@ impl App {
show_spectrum: self.audio.config.show_spectrum,
show_preview: self.audio.config.show_preview,
show_completion: self.ui.show_completion,
performance_mode: self.ui.performance_mode,
color_scheme: self.ui.color_scheme,
layout: self.audio.config.layout,
hue_rotation: self.ui.hue_rotation,

View File

@@ -251,6 +251,7 @@ pub enum AppCommand {
ToggleScope,
ToggleSpectrum,
TogglePreview,
TogglePerformanceMode,
// Metrics
ResetPeakVoices,

View File

@@ -85,6 +85,7 @@ pub fn init(args: InitArgs) -> Init {
app.audio.config.show_spectrum = settings.display.show_spectrum;
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;
app.ui.color_scheme = settings.display.color_scheme;
app.ui.hue_rotation = settings.display.hue_rotation;
app.audio.config.layout = settings.display.layout;

View File

@@ -139,10 +139,7 @@ fn handle_scroll(ctx: &mut InputContext, col: u16, row: u16, term: Rect, up: boo
}
}
Page::Dict => {
let (header_area, [cat_area, words_area]) = dict_view::layout(body);
if contains(header_area, col, row) {
return;
}
let [cat_area, words_area] = dict_view::layout(body);
if contains(cat_area, col, row) {
if up {
ctx.dispatch(AppCommand::DictPrevCategory);
@@ -300,7 +297,7 @@ fn handle_body_click(ctx: &mut InputContext, col: u16, row: u16, body: Rect) {
// --- Main page (grid) ---
fn handle_main_click(ctx: &mut InputContext, col: u16, row: u16, area: Rect) {
let [_patterns_area, _, main_area, _, _vu_area] = main_view::layout(area);
let [main_area, _, _vu_area] = main_view::layout(area);
if !contains(main_area, col, row) {
return;
@@ -533,7 +530,7 @@ fn handle_help_click(ctx: &mut InputContext, col: u16, row: u16, area: Rect) {
// --- Dict page ---
fn handle_dict_click(ctx: &mut InputContext, col: u16, row: u16, area: Rect) {
let (_header_area, [cat_area, words_area]) = dict_view::layout(area);
let [cat_area, words_area] = dict_view::layout(area);
if contains(cat_area, col, row) {
use crate::model::categories::{self, CatEntry, CATEGORIES};

View File

@@ -26,6 +26,7 @@ pub(crate) fn cycle_option_value(ctx: &mut InputContext, right: bool) {
OptionsFocus::ShowSpectrum => ctx.dispatch(AppCommand::ToggleSpectrum),
OptionsFocus::ShowCompletion => ctx.dispatch(AppCommand::ToggleCompletion),
OptionsFocus::ShowPreview => ctx.dispatch(AppCommand::TogglePreview),
OptionsFocus::PerformanceMode => ctx.dispatch(AppCommand::TogglePerformanceMode),
OptionsFocus::Font => {
const FONTS: &[&str] = &["6x13", "7x13", "8x13", "9x15", "9x18", "10x20"];
let pos = FONTS.iter().position(|f| *f == ctx.app.ui.font).unwrap_or(2);

View File

@@ -53,6 +53,8 @@ pub struct DisplaySettings {
#[serde(default)]
pub layout: MainLayout,
#[serde(default)]
pub performance_mode: bool,
#[serde(default)]
pub hue_rotation: f32,
#[serde(default)]
pub onboarding_dismissed: Vec<String>,
@@ -98,6 +100,7 @@ impl Default for DisplaySettings {
show_completion: true,
font: default_font(),
zoom_factor: default_zoom(),
performance_mode: false,
color_scheme: ColorScheme::default(),
layout: MainLayout::default(),
hue_rotation: 0.0,

View File

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

View File

@@ -74,6 +74,7 @@ pub struct UiState {
pub prev_page: Page,
pub prev_show_title: bool,
pub onboarding_dismissed: Vec<String>,
pub performance_mode: bool,
pub font: String,
pub zoom_factor: f32,
pub window_width: u32,
@@ -121,6 +122,7 @@ impl Default for UiState {
prev_page: Page::default(),
prev_show_title: true,
onboarding_dismissed: Vec::new(),
performance_mode: false,
font: "8x13".to_string(),
zoom_factor: 1.5,
window_width: 1200,

View File

@@ -13,40 +13,18 @@ use crate::widgets::{render_search_bar, CategoryItem, CategoryList, Selection};
use CatEntry::{Category, Section};
pub fn layout(area: Rect) -> (Rect, [Rect; 2]) {
let [header_area, body_area] =
Layout::vertical([Constraint::Length(5), Constraint::Fill(1)]).areas(area);
let body = Layout::horizontal([Constraint::Length(16), Constraint::Fill(1)]).areas(body_area);
(header_area, body)
pub fn layout(area: Rect) -> [Rect; 2] {
Layout::horizontal([Constraint::Length(16), Constraint::Fill(1)]).areas(area)
}
pub fn render(frame: &mut Frame, app: &App, area: Rect) {
let (header_area, [cat_area, words_area]) = layout(area);
render_header(frame, header_area);
let [cat_area, words_area] = layout(area);
let is_searching = !app.ui.dict_search_query.is_empty();
render_categories(frame, app, cat_area, is_searching);
render_words(frame, app, words_area, is_searching);
}
fn render_header(frame: &mut Frame, area: Rect) {
use ratatui::widgets::Wrap;
let theme = theme::get();
let desc = "Forth uses a stack: values are pushed, functions (called words) consume and \
produce values. Read left to right: 3 4 + -> push 3, push 4, + pops both, \
pushes 7. This page lists all words with their signature ( inputs -- outputs ).";
let block = Block::default()
.borders(Borders::ALL)
.border_style(Style::new().fg(theme.dict.border_normal))
.title("Dictionary");
let para = Paragraph::new(desc)
.style(Style::new().fg(theme.dict.header_desc))
.wrap(Wrap { trim: false })
.block(block);
frame.render_widget(para, area);
}
fn render_categories(frame: &mut Frame, app: &App, area: Rect, dimmed: bool) {
let theme = theme::get();
let focused = app.ui.dict_focus == DictFocus::Categories && !dimmed;

View File

@@ -13,12 +13,10 @@ use crate::state::MainLayout;
use crate::theme;
use crate::views::highlight::highlight_line_with_runtime;
use crate::views::render::{adjust_resolved_for_line, adjust_spans_for_line};
use crate::widgets::{ActivePatterns, Orientation, Scope, Spectrum, VuMeter};
use crate::widgets::{Orientation, Scope, Spectrum, VuMeter};
pub fn layout(area: Rect) -> [Rect; 5] {
pub fn layout(area: Rect) -> [Rect; 3] {
Layout::horizontal([
Constraint::Length(13),
Constraint::Length(2),
Constraint::Fill(1),
Constraint::Length(2),
Constraint::Length(10),
@@ -27,7 +25,7 @@ pub fn layout(area: Rect) -> [Rect; 5] {
}
pub fn render(frame: &mut Frame, app: &App, snapshot: &SequencerSnapshot, area: Rect) {
let [patterns_area, _, main_area, _, vu_area] = layout(area);
let [main_area, _, vu_area] = layout(area);
let show_scope = app.audio.config.show_scope;
let show_spectrum = app.audio.config.show_spectrum;
@@ -82,7 +80,6 @@ pub fn render(frame: &mut Frame, app: &App, snapshot: &SequencerSnapshot, area:
render_sequencer(frame, app, snapshot, sequencer_area);
render_vu_meter(frame, app, vu_area);
render_active_patterns(frame, app, snapshot, patterns_area);
}
enum VizPanel {
@@ -428,45 +425,3 @@ fn render_vu_meter(frame: &mut Frame, app: &App, area: Rect) {
frame.render_widget(vu, inner);
}
fn render_active_patterns(frame: &mut Frame, app: &App, snapshot: &SequencerSnapshot, area: Rect) {
use crate::widgets::MuteStatus;
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 patterns: Vec<(usize, usize, usize)> = snapshot
.active_patterns
.iter()
.map(|p| (p.bank, p.pattern, p.iter))
.collect();
let mute_status: Vec<MuteStatus> = snapshot
.active_patterns
.iter()
.map(|p| {
if app.mute.is_soloed(p.bank, p.pattern) {
MuteStatus::Soloed
} else if app.mute.is_muted(p.bank, p.pattern) {
MuteStatus::Muted
} else if app.mute.is_effectively_muted(p.bank, p.pattern) {
MuteStatus::EffectivelyMuted
} else {
MuteStatus::Normal
}
})
.collect();
let step_info = snapshot
.get_step(app.editor_ctx.bank, app.editor_ctx.pattern)
.map(|step| (step, app.current_edit_pattern().length));
let mut widget = ActivePatterns::new(&patterns).with_mute_status(&mute_status);
if let Some((step, total)) = step_info {
widget = widget.with_step(step, total);
}
frame.render_widget(widget, inner);
}

View File

@@ -8,7 +8,7 @@ use crate::app::App;
use crate::engine::LinkState;
use crate::midi;
use crate::state::OptionsFocus;
use crate::theme;
use crate::theme::{self, ThemeColors};
use crate::widgets::{render_scroll_indicators, IndicatorAlign};
pub fn render(frame: &mut Frame, app: &App, link: &LinkState, area: Rect) {
@@ -90,6 +90,12 @@ pub fn render(frame: &mut Frame, app: &App, link: &LinkState, area: Rect) {
focus == OptionsFocus::ShowPreview,
&theme,
),
render_option_line(
"Performance mode",
if app.ui.performance_mode { "On" } else { "Off" },
focus == OptionsFocus::PerformanceMode,
&theme,
),
];
if app.plugin_mode {
let zoom_str = format!("{:.0}%", app.ui.zoom_factor * 100.0);
@@ -246,6 +252,14 @@ pub fn render(frame: &mut Frame, app: &App, link: &LinkState, area: Rect) {
]);
}
// Insert description below focused option
let focus_vec_idx = focus.line_index(app.plugin_mode);
if let Some(desc) = option_description(focus) {
if focus_vec_idx < lines.len() {
lines.insert(focus_vec_idx + 1, render_description_line(desc, &theme));
}
}
let total_lines = lines.len();
let max_visible = padded.height as usize;
@@ -318,6 +332,42 @@ fn render_option_line(label: &str, value: &str, focused: bool, theme: &theme::Th
])
}
fn option_description(focus: OptionsFocus) -> Option<&'static str> {
match focus {
OptionsFocus::ColorScheme => Some("Color scheme for the entire interface"),
OptionsFocus::HueRotation => Some("Shift all theme colors by a hue angle"),
OptionsFocus::RefreshRate => Some("Lower values reduce CPU usage"),
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::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"),
OptionsFocus::Font => Some("Bitmap font for the plugin window"),
OptionsFocus::ZoomFactor => Some("Scale factor for the plugin window"),
OptionsFocus::WindowSize => Some("Default size for the plugin window"),
OptionsFocus::LinkEnabled => Some("Join an Ableton Link session on the local network"),
OptionsFocus::StartStopSync => Some("Sync transport start/stop with other Link peers"),
OptionsFocus::Quantum => Some("Number of beats per phase cycle"),
OptionsFocus::MidiOutput0 => Some("MIDI output device for channel group 1"),
OptionsFocus::MidiOutput1 => Some("MIDI output device for channel group 2"),
OptionsFocus::MidiOutput2 => Some("MIDI output device for channel group 3"),
OptionsFocus::MidiOutput3 => Some("MIDI output device for channel group 4"),
OptionsFocus::MidiInput0 => Some("MIDI input device for channel group 1"),
OptionsFocus::MidiInput1 => Some("MIDI input device for channel group 2"),
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"),
}
}
fn render_description_line(desc: &str, theme: &ThemeColors) -> Line<'static> {
Line::from(Span::styled(
format!(" {desc}"),
Style::new().fg(theme.ui.text_dim),
))
}
fn render_readonly_line(label: &str, value: &str, value_style: Style, theme: &theme::ThemeColors) -> Line<'static> {
let label_style = Style::new().fg(theme.ui.text_muted);
let label_width = 20;

View File

@@ -97,16 +97,20 @@ pub fn render(
height: term.height.saturating_sub(2),
};
let perf = app.ui.performance_mode;
let [header_area, _padding, body_area, _bottom_padding, footer_area] = Layout::vertical([
Constraint::Length(header_height(padded.width)),
Constraint::Length(1),
Constraint::Length(if perf { 0 } else { header_height(padded.width) }),
Constraint::Length(if perf { 0 } else { 1 }),
Constraint::Fill(1),
Constraint::Length(1),
Constraint::Length(3),
Constraint::Length(if perf { 0 } else { 1 }),
Constraint::Length(if perf { 0 } else { 3 }),
])
.areas(padded);
render_header(frame, app, link, snapshot, header_area);
if !perf {
render_header(frame, app, link, snapshot, header_area);
}
let (page_area, panel_area) = if app.panel.visible && app.panel.side.is_some() {
if body_area.width >= 120 {
@@ -139,7 +143,9 @@ pub fn render(
render_side_panel(frame, app, side_area);
}
render_footer(frame, app, footer_area);
if !perf {
render_footer(frame, app, footer_area);
}
let modal_area = render_modal(frame, app, snapshot, term);
if app.ui.show_minimap() {

View File

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