Merge pull request #188 from grovebit/window-fixes

Scroll long plugin windows; reopen the main window at its last size and scale
This commit is contained in:
Brandon Thomas authored and GitHub committed 2026-10-11 20:40:58 -04:00
commit 2d7311197d
4 files changed
+120 -40

No files matched your search

+17 -4
View File
@@ -49,6 +49,13 @@ impl eframe::App for App {
if self.0.quit_requested {
ctx.send_viewport_cmd(egui::ViewportCommand::Close);
}
soundcraft_ui_egui::fit_window_to_screen(ctx);
let (inner, maximized) = ctx.input(|i| (i.viewport().inner_rect, i.viewport().maximized.unwrap_or(false)));
self.0.ui.window_maximized = maximized;
if let Some(r) = inner.filter(|_| !maximized) {
self.0.ui.window_size = (r.size() * ctx.zoom_factor()).round().into();
}
self.0.ui.ui_scale = self.0.extra.ui_scale;
// With the system menu bar there is no egui bar showing the window and dirty state, so the
// title carries them; elsewhere the egui menu bar already does.
let title = if self.0.native_menu_bar {
@@ -216,10 +223,12 @@ fn main() -> eframe::Result {
}
let engine = if demo { soundcraft_engine::demo::demo_engine() } else { Engine::default() };
let prefs = load_prefs().unwrap_or_default();
let mut options = eframe::NativeOptions {
viewport: egui::ViewportBuilder::default()
.with_title("SoundCraft")
.with_inner_size([1600.0, 1000.0])
.with_inner_size(sane_window_size(prefs.window_size))
.with_maximized(prefs.window_maximized)
.with_min_inner_size([960.0, 600.0])
.with_drag_and_drop(true)
.with_app_id("ai.storyteller.soundcraft"),
@@ -239,9 +248,8 @@ fn main() -> eframe::Result {
let mut app = SoundApp::new(engine, player, services());
app.autosave_dir = prefs_path().and_then(|p| p.parent().map(|d| d.join("Autosave")));
app.preset_dir = prefs_path().and_then(|p| p.parent().map(|d| d.join("Presets")));
if let Some(ui) = load_prefs() {
app.ui = ui;
}
app.extra.ui_scale = prefs.ui_scale;
app.ui = prefs;
if let Some(port) = control_port {
let rx = control_server::start(port, cc.egui_ctx.clone());
app = app.with_control(rx);
@@ -270,3 +278,8 @@ fn main() -> eframe::Result {
}),
)
}
/// A saved window size, or the default when the prefs file holds nonsense (NaN, negative, tiny).
fn sane_window_size([w, h]: [f32; 2]) -> [f32; 2] {
if w.is_finite() && h.is_finite() && w >= 960.0 && h >= 600.0 { [w.min(16_384.0), h.min(16_384.0)] } else { [1600.0, 1000.0] }
}
+46
View File
@@ -101,6 +101,11 @@ pub struct UiState {
pub configurations: Vec<(String, Value)>,
/// Appearance (`ui.theme`): dark by default, light or follow the system on request.
pub theme: theme::ThemeMode,
/// The main window's size (logical points) and maximized state, and the interface scale, as
/// the desktop app last saw them; it restores them at launch.
pub window_size: [f32; 2],
pub window_maximized: bool,
pub ui_scale: f32,
}
impl Default for UiState {
@@ -153,6 +158,9 @@ impl Default for UiState {
workspace_dir: String::new(),
configurations: Vec::new(),
theme: theme::ThemeMode::Dark,
window_size: [1600.0, 1000.0],
window_maximized: false,
ui_scale: 1.0,
}
}
}
@@ -170,6 +178,15 @@ pub fn preset_folder_name(plugin: &str) -> String {
plugin.chars().map(|c| if c.is_ascii_alphanumeric() || c == '.' || c == '-' || c == '_' { c } else { '_' }).collect()
}
/// On the first frame, maximizes a window too big for the screen it opened on (sized on a larger
/// monitor) instead of letting it run off the edge; its saved size is kept for that monitor.
pub fn fit_window_to_screen(ctx: &egui::Context) {
let (inner, monitor) = ctx.input(|i| (i.viewport().inner_rect, i.viewport().monitor_size));
if ctx.cumulative_pass_nr() == 0 && inner.zip(monitor).is_some_and(|(r, m)| (r.size() - m).max_elem() > 0.0) {
ctx.send_viewport_cmd(egui::ViewportCommand::Maximized(true));
}
}
/// Drag-and-drop payload: an audio file (source id) dragged from the Clip List.
#[derive(Debug, Clone, Copy)]
pub struct DragSource(pub u64);
@@ -1023,6 +1040,35 @@ mod tests {
assert!(automation_step_due(0, 1_000_000, 960));
}
#[test]
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));
}
#[test]
fn a_window_bigger_than_its_screen_opens_maximized() {
let maximizes = |w: f32, frames: usize| {
let ctx = egui::Context::default();
let info = egui::ViewportInfo {
inner_rect: Some(egui::Rect::from_min_size(egui::Pos2::ZERO, egui::vec2(w, 1000.0))),
monitor_size: Some(egui::vec2(1728.0, 1117.0)),
..Default::default()
};
let mut maximized = false;
for _ in 0..frames {
let raw = egui::RawInput { viewports: [(egui::ViewportId::ROOT, info.clone())].into_iter().collect(), ..Default::default() };
let mut out = ctx.run_ui(raw, |ui| super::fit_window_to_screen(ui.ctx()));
out.textures_delta.clear();
maximized = out.viewport_output[&egui::ViewportId::ROOT].commands.contains(&egui::ViewportCommand::Maximized(true));
}
maximized
};
assert!(maximizes(2400.0, 1));
assert!(!maximizes(1600.0, 1));
assert!(!maximizes(2400.0, 2));
}
#[test]
fn ordinary_play_after_stop_is_full_speed() {
use serde_json::json;
+38 -36
View File
@@ -412,44 +412,46 @@ fn plugin_windows(app: &mut SoundApp, ctx: &egui::Context) {
if info.id == "eq_7band" || info.id == "eq_1band" {
eq_curve(ui, info.id, &ins);
}
egui::Grid::new(("pg", tid.0, slot)).num_columns(3).show(ui, |ui| {
for p in info.params {
let v = ins.params.get(p.id).copied().unwrap_or(p.default);
ui.label(p.name);
let mut x = v;
let resp = if !p.choices.is_empty() {
let mut idx = x.round().max(0.0) as usize;
let r = egui::ComboBox::from_id_salt(("pc", tid.0, slot, p.id))
.selected_text(p.choices.get(idx).copied().unwrap_or("?"))
.show_ui(ui, |ui| {
for (i, c) in p.choices.iter().enumerate() {
ui.selectable_value(&mut idx, i, *c);
}
});
x = idx as f32;
r.response
} else {
let mut sl = egui::Slider::new(&mut x, p.min..=p.max).show_value(false);
if p.taper == soundcraft_dsp::Taper::Log && p.min > 0.0 {
sl = sl.logarithmic(true);
}
ui.add(sl)
};
if (x - v).abs() > f32::EPSILON {
let _ = app.engine.execute_merged(
"mix.insert_param",
&json!({"track": tid.0, "slot": slot, "param": p.id, "value": x}),
&format!("param:{}:{slot}:{}", tid.0, p.id),
);
if let Some(pl) = &app.player {
// Keep the plugin's own editor in step.
pl.editor_set_param(tid, slot, p.id, x);
egui::ScrollArea::vertical().show(ui, |ui| {
egui::Grid::new(("pg", tid.0, slot)).num_columns(3).show(ui, |ui| {
for p in info.params {
let v = ins.params.get(p.id).copied().unwrap_or(p.default);
ui.label(p.name);
let mut x = v;
let resp = if !p.choices.is_empty() {
let mut idx = x.round().max(0.0) as usize;
let r = egui::ComboBox::from_id_salt(("pc", tid.0, slot, p.id))
.selected_text(p.choices.get(idx).copied().unwrap_or("?"))
.show_ui(ui, |ui| {
for (i, c) in p.choices.iter().enumerate() {
ui.selectable_value(&mut idx, i, *c);
}
});
x = idx as f32;
r.response
} else {
let mut sl = egui::Slider::new(&mut x, p.min..=p.max).show_value(false);
if p.taper == soundcraft_dsp::Taper::Log && p.min > 0.0 {
sl = sl.logarithmic(true);
}
ui.add(sl)
};
if (x - v).abs() > f32::EPSILON {
let _ = app.engine.execute_merged(
"mix.insert_param",
&json!({"track": tid.0, "slot": slot, "param": p.id, "value": x}),
&format!("param:{}:{slot}:{}", tid.0, p.id),
);
if let Some(pl) = &app.player {
// Keep the plugin's own editor in step.
pl.editor_set_param(tid, slot, p.id, x);
}
}
let _ = resp;
ui.label(egui::RichText::new(p.format(x)).font(mono(11.0)));
ui.end_row();
}
let _ = resp;
ui.label(egui::RichText::new(p.format(x)).font(mono(11.0)));
ui.end_row();
}
});
});
});
if open {
+19
View File
@@ -0,0 +1,19 @@
use egui_kittest::Harness;
use soundcraft_ui_egui::{Services, SoundApp};
#[test]
fn a_long_plugin_window_stays_on_a_short_screen() {
let mut app = SoundApp::new(soundcraft_engine::demo::demo_engine(), None, Services::default());
let kick = app.engine.session().track_by_name("Kick").unwrap().id;
app.ui.plugin_windows = vec![(kick, 0)];
let mut h = Harness::builder().with_size(egui::vec2(1200.0, 500.0)).build_ui_state(
|ui, app: &mut SoundApp| {
app.logic(&ui.ctx().clone());
app.ui(ui);
},
app,
);
h.run_steps(4);
let window = h.ctx.memory(|m| m.area_rect(egui::Id::new(("plugin", kick.0, 0usize)))).unwrap();
assert!(window.bottom() <= 500.0, "{window:?}");
}