mirror of
https://github.com/aphrody-labs/soundcraft.git
synced 2026-10-13 19:26:53 +00:00
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:
4 files changed
+120
-40
No files matched your search
@@ -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] }
|
||||
}
|
||||
@@ -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;
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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:?}");
|
||||
}
|
||||
Reference in new issue
Block a user