feat(ui): add complete Preferences window and live settings (#819)

* feat(ui): add complete Preferences window shell

* fix(ui): refine Preferences layout and live settings

* fix(ui): remove Preferences page scrolling

* fix(ui): keep Preferences window size constant

* feat(ui): add Preferences to the SoundCraft menu

* fix(ui): address Preferences review feedback

* docs(ui): document Preferences behavior

* fix(macOS): add Preferences to the application menu

* fix(ui): make Preferences menu behavior modal

* fix(ui): preserve live Preferences updates

* fix(ui): address latest Preferences review findings

* fix(ui): preserve Preferences button actions
This commit is contained in:
SlyLaM authored and GitHub committed 2026-11-06 11:34:43 +01:00
1 parent 87f46cf23b
commit e66a5e7c67
11 files changed
+1656 -55

No files matched your search

+3 -2
View File
@@ -33,6 +33,7 @@ impl NativeMenu {
let mut native = Self { menu, entries: Vec::new(), events, last_sync: None };
let app_menu = Submenu::new("SoundCraft", true);
native.add_item(app, &app_menu, "About SoundCraft", "", Some("window.about".into()))?;
native.add_item(app, &app_menu, "Preferences...", "", Some("setup.preferences".into()))?;
native.add_item(app, &app_menu, "Session Info", "", Some("window.session_info".into()))?;
native.add_item(app, &app_menu, "Session Audio Health", "", Some("window.audio_health".into()))?;
app_menu.append(&PredefinedMenuItem::separator())?;
@@ -94,8 +95,8 @@ impl NativeMenu {
&& let Some(command) = &entry.command
&& menus::item_state(app, &entry.path, Some(command)).0
{
if entry.path.is_empty() && command.starts_with("window.") {
// The application menu's About / Session Info open their window (never close it).
if entry.path.is_empty() && (command.starts_with("window.") || command == "setup.preferences") {
// Application-menu window entries open their window rather than toggling it.
let _ = app.run(command, serde_json::json!({"value": true}));
} else {
menus::invoke_menu(app, command, &entry.path);
+63
View File
@@ -343,6 +343,16 @@ impl Engine {
self.undo.iter().map(|h| h.label.clone()).collect()
}
/// Bound Undo and Redo together, dropping farthest redo entries before oldest undo entries.
pub fn set_history_limit(&mut self, limit: usize) {
let limit = limit.max(1);
self.history_limit = limit;
let excess = self.undo.len().saturating_add(self.redo.len()).saturating_sub(limit);
let from_redo = excess.min(self.redo.len());
self.redo.drain(..from_redo);
self.undo.drain(..excess - from_redo);
}
/// Restore the previous document while retaining current edit mode and live input flags.
pub fn undo(&mut self) -> bool {
let Some(h) = self.undo.pop() else { return false };
@@ -716,6 +726,59 @@ mod merge_tests {
mod dirty_tests {
use super::*;
/// Lowering the cap immediately removes the oldest retained Undo steps.
#[test]
fn reducing_the_history_limit_drops_the_oldest_steps_immediately() {
let mut e = Engine::default();
for _ in 0..4 {
e.execute("track.new", &json!({})).unwrap();
}
assert_eq!(e.undo_history().len(), 4);
e.set_history_limit(2);
assert_eq!(e.undo_history().len(), 2);
assert!(e.undo());
assert!(e.undo());
assert!(!e.undo());
}
/// A shared history cap retains the closest redo steps without letting either direction exceed it.
#[test]
fn lowering_history_limit_bounds_undo_and_redo_together() {
for (requested, past, future) in [(0, 1, 0), (1, 1, 0), (2, 2, 0), (3, 2, 1), (4, 2, 2), (5, 2, 3), (usize::MAX, 2, 3)] {
let mut e = Engine::default();
for _ in 0..5 {
e.execute("track.new", &json!({})).unwrap();
}
for _ in 0..3 {
assert!(e.undo());
}
e.mark_clean();
e.set_history_limit(requested);
let limit = requested.max(1);
assert_eq!(e.history_limit, limit);
assert_eq!((e.undo.len(), e.redo.len()), (past, future), "requested {requested}");
assert!(!e.is_dirty(), "history capacity is not a document edit");
assert_eq!(e.session().tracks.len(), 2);
for step in 1..=future {
assert!(e.redo());
assert_eq!(e.session().tracks.len(), 2 + step, "keep the nearest redo entries");
assert!(e.undo.len() + e.redo.len() <= limit);
}
assert!(!e.redo());
for step in 1..=past + future {
assert!(e.undo());
assert_eq!(e.session().tracks.len(), 2 + future - step, "keep the nearest undo entries");
assert!(e.undo.len() + e.redo.len() <= limit);
}
assert!(!e.undo());
for _ in 0..past + future {
assert!(e.redo());
assert!(e.undo.len() + e.redo.len() <= limit);
}
assert!(!e.redo());
}
}
#[test]
fn selecting_locating_and_zooming_leave_the_session_clean() {
let mut e = crate::demo::demo_engine();
+5 -1
View File
@@ -3,7 +3,7 @@ use super::state::MainWindow;
use super::{
ControlRequest, Engine, Gesture, HashMap, MeterDisplay, OutputTrouble, Player, Range, Receiver, Samples, SoundApp, TrackId, TransportRequest,
Trouble, UiState, Value, VecDeque, bounce, dialogs, edit_window, extra_windows, fonts, import, json, menus, midi_editor, mix_window, ops_windows,
palette, panels, recovery, sampler_window, score_editor, shortcuts, theme, updates_ui, video_track, video_window, windows,
palette, panels, preferences, recovery, sampler_window, score_editor, shortcuts, theme, updates_ui, video_track, video_window, windows,
};
mod audio;
@@ -63,6 +63,7 @@ impl SoundApp {
ops: ops_windows::OpsState::default(),
extra: extra_windows::ExtraState::default(),
palette: palette::PaletteState::default(),
preferences: preferences::PreferencesState::default(),
focus_mix: false,
edit_scroll_into_view: None,
mix_scroll_into_view: None,
@@ -137,6 +138,8 @@ impl SoundApp {
/// Each host iteration starts with `raw_input_hook`; its editor-poll admission prevents an
/// empty queue on a discarded pass from ending a still-active plugin gesture.
pub fn logic(&mut self, ctx: &egui::Context) {
self.ui.undo_levels = self.ui.undo_levels.clamp(1, 64);
self.engine.set_history_limit(self.ui.undo_levels);
// Fonts set now take effect next frame, so draw only from the frame after.
if !self.fonts_ready {
if self.fonts_installed {
@@ -333,6 +336,7 @@ impl SoundApp {
}
panels::floating(self, &ctx);
windows::show(self, &ctx);
preferences::show(self, &ctx);
video_window::show(self, &ctx);
ops_windows::show(self, &ctx);
extra_windows::show(self, &ctx);
+14
View File
@@ -27,6 +27,7 @@ pub mod menus;
pub mod mix_window;
pub mod palette;
pub mod panels;
pub mod preferences;
pub mod shortcuts;
pub mod theme;
pub mod toolbar;
@@ -114,6 +115,13 @@ pub struct UiState {
pub show_workspace: bool,
pub show_configurations: bool,
pub show_playback_engine: bool,
/// Setup › Preferences. The selected page is kept per machine; the search and staged values
/// live in [`preferences::PreferencesState`] and are discarded when the window closes.
pub show_preferences: bool,
pub preferences_page: preferences::PreferencesPage,
pub preferences_window_pos: Option<[f32; 2]>,
pub reduce_visual_effects: preferences::ReduceVisualEffects,
pub undo_levels: usize,
/// The audio device choice (Setup › Playback Engine), kept with the preferences: per machine.
pub audio: soundcraft_playback::AudioConfig,
/// The Mix window lives in its own native window (Window › Mix in Separate Window); the main
@@ -182,6 +190,11 @@ impl Default for UiState {
show_workspace: false,
show_configurations: false,
show_playback_engine: false,
show_preferences: false,
preferences_page: preferences::PreferencesPage::default(),
preferences_window_pos: None,
reduce_visual_effects: preferences::ReduceVisualEffects::default(),
undo_levels: 64,
audio: soundcraft_playback::AudioConfig::default(),
mix_separate: false,
show_io_setup: false,
@@ -302,6 +315,7 @@ pub struct SoundApp {
pub ops: ops_windows::OpsState,
pub extra: extra_windows::ExtraState,
pub palette: palette::PaletteState,
pub preferences: preferences::PreferencesState,
/// Bring the separate Mix window to the front on the next frame.
pub(crate) focus_mix: bool,
/// Scroll Into View: the track each window brings to its top (Edit) or left (Mix) when it
+150 -45
View File
@@ -75,6 +75,8 @@ pub const UI_COMMANDS: &[(&str, &str, &str, Option<&str>)] = &[
("ui.bounce_dialog", "Bounce Mix…", "", None),
("ui.set", "Set UI State", "", None),
("ui.theme", "Appearance", "", None),
("ui.reduce_visual_effects", "Reduce Visual Effects", "", None),
("ui.undo_levels", "Levels of Undo", "", None),
];
/// Extra catalog mappings handled by the UI layer.
@@ -225,56 +227,65 @@ pub fn tree() -> Vec<MenuNode> {
pub fn menu_bar(app: &mut SoundApp, ui: &mut egui::Ui) {
let t = crate::theme::Tokens::current();
let menu_entries_enabled = !app.ui.show_preferences;
let preferences_enabled = preferences_menu_enabled(app);
egui::Panel::top("menu_bar").exact_size(24.0).frame(egui::Frame::NONE.fill(t.menu_bar).inner_margin(egui::Margin::symmetric(8, 2))).show(
ui,
|ui| {
egui::MenuBar::new().ui(ui, |ui| {
ui.menu_button("SoundCraft", |ui| {
if ui.button("About SoundCraft").clicked() {
app.ui.show_about = true;
}
if ui.button("Check for Updates…").clicked() {
let _ = app.run("app.checkForUpdates", json!({}));
}
if ui.button("Session Info").clicked() {
app.ui.show_session_info = true;
}
if ui.button("Session Audio Health").clicked() {
let _ = app.run("window.audio_health", json!({"value": true}));
}
ui.separator();
if ui.button("Quit").clicked() {
let _ = crate::dialogs::run_or_ask(app, "app.quit", json!({}));
}
ui.add_enabled_ui(menu_entries_enabled, |ui| {
if ui.button("About SoundCraft").clicked() {
app.ui.show_about = true;
}
if ui.add_enabled(preferences_enabled, egui::Button::new("Preferences...")).clicked() {
let _ = app.run("setup.preferences", json!({"value": true}));
}
if ui.button("Check for Updates…").clicked() {
let _ = app.run("app.checkForUpdates", json!({}));
}
if ui.button("Session Info").clicked() {
app.ui.show_session_info = true;
}
if ui.button("Session Audio Health").clicked() {
let _ = app.run("window.audio_health", json!({"value": true}));
}
ui.separator();
if ui.button("Quit").clicked() {
let _ = crate::dialogs::run_or_ask(app, "app.quit", json!({}));
}
});
});
let extra = ui_aliases();
for root in tree() {
ui.menu_button(&root.label, |ui| {
for c in &root.children {
menu_node(app, ui, c, &extra);
if c.path == "File > Open" {
crate::recent::menu(app, ui);
ui.add_enabled_ui(menu_entries_enabled, |ui| {
for c in &root.children {
menu_node(app, ui, c, &extra);
if c.path == "File > Open" {
crate::recent::menu(app, ui);
}
if c.path == "Track > New..." {
instrument_menu(app, ui);
}
}
if c.path == "Track > New..." {
instrument_menu(app, ui);
if root.label == "AudioSuite" {
ui.separator();
ui.label(egui::RichText::new("Processes selected clips offline").small().color(t.text_dim));
}
}
if root.label == "AudioSuite" {
ui.separator();
ui.label(egui::RichText::new("Processes selected clips offline").small().color(t.text_dim));
}
if root.label == "Window" {
ui.separator();
let mut separate = app.ui.mix_separate;
if ui
.checkbox(&mut separate, "Mix in Separate Window")
.on_hover_text("Show Edit and Mix at once, e.g. one per monitor")
.changed()
{
let _ = run_ui_command(app, "window.mix_separate", &json!({"value": separate}));
ui.close();
if root.label == "Window" {
ui.separator();
let mut separate = app.ui.mix_separate;
if ui
.checkbox(&mut separate, "Mix in Separate Window")
.on_hover_text("Show Edit and Mix at once, e.g. one per monitor")
.changed()
{
let _ = run_ui_command(app, "window.mix_separate", &json!({"value": separate}));
ui.close();
}
}
}
});
});
}
let title = format!(
@@ -338,18 +349,31 @@ pub fn command_for_path(path: &str, extra: &[(&str, &str)]) -> Option<String> {
/// Enabled and checked state shared by all menu presentations.
pub fn item_state(app: &SoundApp, path: &str, id: Option<&str>) -> (bool, bool) {
let enabled = match id {
Some(i)
if i.starts_with("window.") || i.starts_with("view.mix_section") || matches!(i, "audiosuite.process" | "ui.dock" | "ui.dock.reset") =>
{
true
let enabled = if app.ui.show_preferences {
false
} else {
match id {
Some("setup.preferences") => preferences_menu_enabled(app),
Some(i)
if i.starts_with("window.")
|| i.starts_with("view.mix_section")
|| matches!(i, "audiosuite.process" | "ui.dock" | "ui.dock.reset") =>
{
true
}
Some(i) => soundcraft_engine::find_command(i).is_some_and(|c| (c.enabled)(&app.engine).is_ok() && !app.engine.blocked_while_recording(i)),
None => false,
}
Some(i) => soundcraft_engine::find_command(i).is_some_and(|c| (c.enabled)(&app.engine).is_ok() && !app.engine.blocked_while_recording(i)),
None => false,
};
(enabled, checked_state(app, path, id))
}
/// Preferences cannot replace audio resources while transport is active or another Preferences
/// instance is already modal.
fn preferences_menu_enabled(app: &SoundApp) -> bool {
!app.ui.show_preferences && !app.is_playing() && !app.engine.transport.recording
}
fn checked_state(app: &SoundApp, path: &str, id: Option<&str>) -> bool {
let e = &app.engine.session().edit;
match id {
@@ -469,6 +493,10 @@ pub fn live_label(app: &SoundApp, id: Option<&str>) -> Option<String> {
/// Menu-style invocation: may open a dialog.
pub fn invoke_menu(app: &mut SoundApp, id: &str, path: &str) {
if id == "setup.preferences" {
let _ = app.run(id, json!({"value": true}));
return;
}
if let Some((_, w)) = MENU_WINDOWS.iter().find(|(p, _)| *p == path)
&& w.starts_with("window.")
{
@@ -587,6 +615,15 @@ pub fn run_ui_command(app: &mut SoundApp, id: &str, p: &Value) -> Option<Result<
"window.midi_ops" => toggle(&mut app.ui.show_midi_ops),
"window.rtp" => toggle(&mut app.ui.show_rtp),
"window.playback_engine" => toggle(&mut app.ui.show_playback_engine),
// The engine catalog already exposes this command. Handling it here keeps the menu,
// control channel and scripted callers on the same public command id.
"setup.preferences" => {
let result = toggle(&mut app.ui.show_preferences);
if !app.ui.show_preferences {
app.preferences.discard_draft();
}
result
}
"window.io_setup" => toggle(&mut app.ui.show_io_setup),
"window.shortcuts" => toggle(&mut app.ui.show_shortcuts),
"window.color_palette" => toggle(&mut app.ui.show_color_palette),
@@ -640,6 +677,8 @@ pub fn run_ui_command(app: &mut SoundApp, id: &str, p: &Value) -> Option<Result<
app.ui.show_undo_history = false;
app.ui.show_session_info = false;
app.ui.show_audio_health = false;
app.ui.show_preferences = false;
app.preferences.discard_draft();
app.ui.show_automation = false;
app.ui.plugin_windows.clear();
app.ui.audiosuite = None;
@@ -694,6 +733,23 @@ pub fn run_ui_command(app: &mut SoundApp, id: &str, p: &Value) -> Option<Result<
app.ui.theme = mode;
json!({"mode": mode.id()})
}
"ui.reduce_visual_effects" => {
let Some(mode) = p.get("mode").and_then(Value::as_str) else {
return Some(Ok(json!({"mode": app.ui.reduce_visual_effects.id()})));
};
let Some(mode) = crate::preferences::ReduceVisualEffects::parse(mode) else {
return Some(Err(format!("unknown visual-effects mode `{mode}` (never, playback or always)")));
};
app.ui.reduce_visual_effects = mode;
json!({"mode": mode.id()})
}
"ui.undo_levels" => {
if let Some(levels) = p.get("levels").and_then(Value::as_u64) {
app.ui.undo_levels = usize::try_from(levels).unwrap_or(64).clamp(1, 64);
app.engine.set_history_limit(app.ui.undo_levels);
}
json!({"levels": app.ui.undo_levels})
}
"ui.set" => match serde_json::to_value(&app.ui) {
Ok(mut cur) => {
if let (Some(obj), Some(patch)) = (cur.as_object_mut(), p.as_object()) {
@@ -706,8 +762,14 @@ pub fn run_ui_command(app: &mut SoundApp, id: &str, p: &Value) -> Option<Result<
if let Err(error) = crate::docking::validate(&ns.docking).and_then(|()| crate::docking::validate_mix(&ns.mix_docking)) {
return Some(Err(error));
}
let closed_preferences = app.ui.show_preferences && !ns.show_preferences;
crate::docking::cancel_replaced_state(app, &ns);
app.ui = ns;
app.ui.undo_levels = app.ui.undo_levels.clamp(1, 64);
app.engine.set_history_limit(app.ui.undo_levels);
if closed_preferences {
app.preferences.discard_draft();
}
json!({"ui": app.ui})
}
Err(e) => return Some(Err(format!("bad ui state: {e}"))),
@@ -731,6 +793,49 @@ mod tests {
use super::*;
use crate::{Services, SoundApp};
/// Preferences menu entries follow transport safety and the modal window locks every menu.
#[test]
fn preferences_menu_state_tracks_transport_and_modality() {
let mut app = SoundApp::new(soundcraft_engine::Engine::default(), None, Services::default());
assert!(item_state(&app, "Setup > Preferences...", Some("setup.preferences")).0);
app.engine.transport.playing = true;
assert!(!item_state(&app, "Setup > Preferences...", Some("setup.preferences")).0);
app.engine.transport.playing = false;
app.engine.transport.recording = true;
assert!(!item_state(&app, "Setup > Preferences...", Some("setup.preferences")).0);
app.engine.transport.recording = false;
app.ui.show_preferences = true;
assert!(!item_state(&app, "Setup > Preferences...", Some("setup.preferences")).0);
assert!(!item_state(&app, "Window > About", Some("window.about")).0);
assert!(!item_state(&app, "File > Save Session", Some("session.save")).0);
}
/// Re-selecting Preferences through either menu keeps its modal window open.
#[test]
fn preferences_menu_always_opens_instead_of_toggling() {
let mut app = SoundApp::new(soundcraft_engine::Engine::default(), None, Services::default());
invoke_menu(&mut app, "setup.preferences", "Setup > Preferences...");
assert!(app.ui.show_preferences);
invoke_menu(&mut app, "setup.preferences", "Setup > Preferences...");
assert!(app.ui.show_preferences);
}
/// Generic UI-state updates clamp and immediately synchronize the engine history limit.
#[test]
fn ui_set_synchronizes_undo_levels_immediately() {
let mut app = SoundApp::new(soundcraft_engine::Engine::default(), None, Services::default());
let result = app.run("ui.set", json!({"undo_levels": 0})).unwrap();
assert_eq!(result["ui"]["undo_levels"], 1);
assert_eq!((app.ui.undo_levels, app.engine.history_limit), (1, 1));
let result = app.run("ui.set", json!({"undo_levels": 100})).unwrap();
assert_eq!(result["ui"]["undo_levels"], 64);
assert_eq!((app.ui.undo_levels, app.engine.history_limit), (64, 64));
}
#[test]
fn mix_view_aliases_are_allocated_once() {
let first = ui_aliases();
+18 -5
View File
@@ -16,7 +16,6 @@ pub struct ExtraState {
pub show_ui_customization: bool,
pub auto_update_config: bool,
pub ui_scale: f32,
pub reduce_motion: bool,
pub tooltips: bool,
last_auto_update: f64,
applied: Option<(f32, bool)>,
@@ -29,7 +28,6 @@ impl Default for ExtraState {
show_ui_customization: false,
auto_update_config: false,
ui_scale: 1.0,
reduce_motion: false,
tooltips: true,
last_auto_update: 0.0,
applied: None,
@@ -59,7 +57,12 @@ pub fn show(app: &mut SoundApp, ctx: &egui::Context) {
}
fn apply_customization(app: &mut SoundApp, ctx: &egui::Context) {
let want = (app.extra.ui_scale.clamp(0.75, 1.5), app.extra.reduce_motion);
let reduce = match app.ui.reduce_visual_effects {
crate::preferences::ReduceVisualEffects::Never => false,
crate::preferences::ReduceVisualEffects::Playback => app.is_playing(),
crate::preferences::ReduceVisualEffects::Always => true,
};
let want = (app.extra.ui_scale.clamp(0.75, 1.5), reduce);
// Rescaling mid-drag would move the Interface scale slider out from under the pointer.
if app.extra.applied == Some(want) || ctx.input(|i| i.pointer.any_down()) {
return;
@@ -163,7 +166,17 @@ fn ui_customization(app: &mut SoundApp, ctx: &egui::Context) {
.custom_formatter(|v, _| format!("{:.0} %", v * 100.0))
.text("Interface scale"),
);
crate::widgets::checkbox(ui, &mut app.extra.reduce_motion, "Reduce motion");
let mut chosen_effects = None;
craft_ui::ComboBox::from_label("Reduce visual effects").selected_text(app.ui.reduce_visual_effects.label()).show_ui(ui, |ui| {
for mode in crate::preferences::ReduceVisualEffects::ALL {
if ui.selectable_label(app.ui.reduce_visual_effects == mode, mode.label()).clicked() {
chosen_effects = Some(mode);
}
}
});
if let Some(mode) = chosen_effects {
let _ = app.run("ui.reduce_visual_effects", json!({"mode": mode.id()}));
}
let mut chosen = None;
craft_ui::ComboBox::from_label("Theme").selected_text(app.ui.theme.label()).show_ui(ui, |ui| {
for mode in crate::theme::ThemeMode::ALL {
@@ -179,7 +192,7 @@ fn ui_customization(app: &mut SoundApp, ctx: &egui::Context) {
ui.add_space(4.0);
if ui.button("Reset").clicked() {
app.extra.ui_scale = 1.0;
app.extra.reduce_motion = false;
let _ = app.run("ui.reduce_visual_effects", json!({"mode": "never"}));
let _ = app.run("ui.theme", json!({"mode": crate::theme::ThemeMode::default().id()}));
}
},
+7 -1
View File
@@ -408,7 +408,7 @@ fn configurations(app: &mut SoundApp, ctx: &egui::Context) {
app.ui.show_configurations = open;
}
/// Restore a saved window configuration, keeping the configuration list and the appearance.
/// Restore a window layout while retaining current application preferences and any open draft.
/// A configuration saved before the current panel layouts gets the default layouts.
pub fn recall_configuration(app: &mut SoundApp, saved: &Value) -> Result<(), String> {
let mut saved = saved.clone();
@@ -417,8 +417,14 @@ pub fn recall_configuration(app: &mut SoundApp, saved: &Value) -> Result<(), Str
crate::docking::validate(&st.docking).and_then(|()| crate::docking::validate_mix(&st.mix_docking))?;
st.configurations = app.ui.configurations.clone();
st.theme = app.ui.theme;
st.show_preferences = app.ui.show_preferences;
st.preferences_page = app.ui.preferences_page;
st.preferences_window_pos = app.ui.preferences_window_pos;
st.reduce_visual_effects = app.ui.reduce_visual_effects;
st.undo_levels = app.ui.undo_levels.clamp(1, 64);
st.show_configurations = true;
crate::docking::cancel_replaced_state(app, &st);
app.engine.set_history_limit(st.undo_levels);
app.ui = st;
Ok(())
}
File diff suppressed because it is too large. Load diff
+1 -1
View File
@@ -59,7 +59,7 @@ fn search_chord(m: Modifiers, mac: bool) -> bool {
pub fn handle(app: &mut SoundApp, ctx: &egui::Context) {
// Cmd+= and Cmd+0 are Mix/Edit and Quantize to Grid, not egui's built-in interface zoom.
ctx.options_mut(|o| o.zoom_with_keyboard = false);
if ctx.egui_wants_keyboard_input() || app.dialogs.open.is_some() {
if ctx.egui_wants_keyboard_input() || app.dialogs.open.is_some() || app.ui.show_preferences {
return;
}
// A scripted press is identified by its exact position in this pass's events, not by matching
+2
View File
@@ -457,6 +457,8 @@ fn a_preset_changed_in_an_effect_editor_is_stored_once() {
fn preferences_saved_before_the_window_was_remembered_keep_full_scale() {
let ui: super::UiState = serde_json::from_value(json!({"show_transport": true})).unwrap();
assert_eq!((ui.window_size, ui.window_maximized, ui.ui_scale), ([1600.0, 1000.0], false, 1.0));
assert_eq!(ui.reduce_visual_effects, crate::preferences::ReduceVisualEffects::Never);
assert_eq!(ui.undo_levels, 64);
}
#[test]
+2
View File
@@ -72,6 +72,8 @@ Legend: ✅ matches · 🟡 partial · ❌ missing.
|---|---|---|
| 2026-10-25 | minor | Zoom Toggle row: Alt+Shift+E and Alt-click cancel Zoom Toggle and keep the view. Readiness estimate unchanged |
| 2026-10-25 | minor | Edit and Mix windows: Tracks and Groups and Clips in full-height dock columns. Readiness estimate unchanged |
| 2026-10-20 | minor | Preferences uses one fixed non-scrolling size with compact stacked choices, removes the duplicate meter-type list, and command-backs visual-effects reduction and undo depth alongside the UI theme |
| 2026-10-19 | minor | Preferences opens from Setup with nine searchable pages, compact grouped controls, staged apply/cancel, platform tab shortcuts and disabled placeholders for unavailable settings |
| 2026-10-16 | minor | Track name menu: Scroll Into View brings the track to the top of the Edit window and the left of the Mix. Readiness estimate unchanged |
| 2026-10-13 | minor | A plugin opens when it's inserted (#486) |
| 2026-10-13 | minor | Drag across checkboxes (#399) |