soundcraft/crates/ui-egui/src/extra_windows.rs

189 lines
8.0 KiB
Rust

//! Window menu extras: the Renderer (speaker layout of the main output with live levels and the
//! object tracks' positions), UI Customization, and auto-updating the active window configuration.
use crate::SoundApp;
use crate::theme::{Tokens, bold, regular};
use egui::{Align2, Color32, Sense, Stroke, pos2, vec2};
use serde_json::{Value, json};
use soundcraft_model::{Speaker, TrackKind};
/// How often an auto-updating configuration is refreshed (seconds).
const AUTO_UPDATE_SECS: f64 = 2.0;
#[derive(Debug, Clone)]
pub struct ExtraState {
pub show_renderer: bool,
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)>,
}
impl Default for ExtraState {
fn default() -> Self {
Self {
show_renderer: false,
show_ui_customization: false,
auto_update_config: false,
ui_scale: 1.0,
reduce_motion: false,
tooltips: true,
last_auto_update: 0.0,
applied: None,
}
}
}
/// UI commands handled here (`window.renderer`, `window.ui_customization`, `window.config_auto_update`).
pub fn run(app: &mut SoundApp, id: &str, p: &Value) -> Option<Value> {
let flag = |v: &mut bool| {
*v = p.get("value").and_then(Value::as_bool).unwrap_or(!*v);
json!({"value": *v})
};
Some(match id {
"window.renderer" => flag(&mut app.extra.show_renderer),
"window.ui_customization" => flag(&mut app.extra.show_ui_customization),
"window.config_auto_update" => flag(&mut app.extra.auto_update_config),
_ => return None,
})
}
pub fn show(app: &mut SoundApp, ctx: &egui::Context) {
auto_update(app, ctx);
renderer(app, ctx);
ui_customization(app, ctx);
apply_customization(app, ctx);
}
fn apply_customization(app: &mut SoundApp, ctx: &egui::Context) {
let want = (app.extra.ui_scale.clamp(0.75, 1.5), app.extra.reduce_motion);
// 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;
}
// Only touch the zoom once the user has changed it, so platform scaling stays untouched.
if app.extra.applied.is_some() || (want.0 - 1.0).abs() > f32::EPSILON {
ctx.set_zoom_factor(want.0);
}
ctx.global_style_mut(|s| s.animation_time = if want.1 { 0.0 } else { 1.0 / 12.0 });
app.extra.applied = Some(want);
}
fn auto_update(app: &mut SoundApp, ctx: &egui::Context) {
if !app.extra.auto_update_config || app.ui.configurations.is_empty() {
return;
}
let now = ctx.input(|i| i.time);
if now - app.extra.last_auto_update >= AUTO_UPDATE_SECS {
app.extra.last_auto_update = now;
let _ = crate::menus::run_ui_command(app, "window.config_update", &json!({}));
}
}
fn renderer(app: &mut SoundApp, ctx: &egui::Context) {
let mut open = app.extra.show_renderer;
if !open {
return;
}
let t = Tokens::current();
let fmt = app.engine.session().main_format();
let peaks: Vec<f32> = app.player.as_ref().map(|p| p.meters().main.peaks).unwrap_or_default();
let objects: Vec<(String, f32, f32, f32)> = app
.engine
.session()
.tracks
.iter()
.filter(|tr| tr.kind != TrackKind::Master && app.engine.session().edit.flag(&format!("object.{}", tr.id.0)))
.map(|tr| {
let sp = tr.mixer.surround.unwrap_or_default();
(tr.name.clone(), sp.x, sp.y, sp.z)
})
.collect();
egui::Window::new(egui::RichText::new("Renderer").font(bold(13.0))).open(&mut open).default_size(vec2(380.0, 440.0)).resizable(true).show(
ctx,
|ui| {
ui.label(egui::RichText::new(format!("Main output: {}", fmt.label())).font(regular(11.0)).color(t.text));
let side = ui.available_width().min(360.0);
let (r, _) = ui.allocate_exact_size(vec2(side, side), Sense::hover());
let p = ui.painter_at(r);
p.rect_filled(r, 6.0, Color32::from_rgb(16, 19, 23));
let c = r.center();
let rad = side * 0.40;
p.circle_stroke(c, rad, Stroke::new(1.0, Color32::from_rgb(60, 68, 78)));
p.circle_stroke(c, rad * 0.55, Stroke::new(1.0, Color32::from_rgb(44, 50, 58)));
p.text(pos2(c.x, r.min.y + 8.0), Align2::CENTER_TOP, "front", regular(9.0), t.text_dim);
for (i, sp) in fmt.speakers().iter().enumerate() {
let level = peaks.get(i).copied().unwrap_or(0.0).clamp(0.0, 1.0);
let ring = if sp.is_height() { rad * 0.55 } else { rad };
let a = sp.azimuth().to_radians();
let pos = if *sp == Speaker::Lfe { pos2(c.x, c.y + rad * 0.2) } else { pos2(c.x + ring * a.sin(), c.y - ring * a.cos()) };
let glow = Color32::from_rgb(40 + (level * 60.0) as u8, 90 + (level * 140.0) as u8, 70 + (level * 40.0) as u8);
p.circle_filled(pos, 9.0 + level * 6.0, glow);
p.circle_stroke(pos, 9.0, Stroke::new(1.0, Color32::from_rgb(150, 160, 170)));
p.text(pos, Align2::CENTER_CENTER, format!("{sp:?}"), bold(8.0), Color32::WHITE);
}
for (name, x, y, z) in &objects {
let pos = pos2(c.x + x.clamp(-1.0, 1.0) * rad, c.y - y.clamp(-1.0, 1.0) * rad);
p.circle_filled(pos, 5.0 + z.clamp(0.0, 1.0) * 3.0, Color32::from_rgb(240, 180, 60));
p.text(pos2(pos.x + 8.0, pos.y), Align2::LEFT_CENTER, name, regular(9.0), Color32::from_rgb(240, 200, 120));
}
ui.add_space(4.0);
if objects.is_empty() {
ui.label(
egui::RichText::new("No object tracks. Use the Object view in the Mix window to route tracks as objects.")
.small()
.color(t.text_dim),
);
} else {
ui.label(egui::RichText::new(format!("{} object track(s)", objects.len())).small().color(t.text_dim));
}
},
);
app.extra.show_renderer = open;
}
fn ui_customization(app: &mut SoundApp, ctx: &egui::Context) {
let mut open = app.extra.show_ui_customization;
if !open {
return;
}
egui::Window::new(egui::RichText::new("Appearance").font(bold(13.0))).open(&mut open).default_size(vec2(320.0, 180.0)).resizable(false).show(
ctx,
|ui| {
egui::Grid::new("ui_custom").num_columns(2).spacing(vec2(12.0, 8.0)).show(ui, |ui| {
ui.label("Interface scale");
ui.add(egui::Slider::new(&mut app.extra.ui_scale, 0.75..=1.5).step_by(0.05).custom_formatter(|v, _| format!("{:.0} %", v * 100.0)));
ui.end_row();
ui.label("Reduce motion");
ui.checkbox(&mut app.extra.reduce_motion, "");
ui.end_row();
ui.label("Theme");
let mut chosen = None;
egui::ComboBox::from_id_salt("theme_mode").selected_text(app.ui.theme.label()).show_ui(ui, |ui| {
for mode in crate::theme::ThemeMode::ALL {
if ui.selectable_label(app.ui.theme == mode, mode.label()).clicked() {
chosen = Some(mode);
}
}
});
if let Some(mode) = chosen {
let _ = app.run("ui.theme", json!({"mode": mode.id()}));
}
ui.end_row();
ui.label("Auto-update configuration");
ui.checkbox(&mut app.extra.auto_update_config, "");
ui.end_row();
});
if ui.button("Reset").clicked() {
app.extra.ui_scale = 1.0;
app.extra.reduce_motion = false;
let _ = app.run("ui.theme", json!({"mode": crate::theme::ThemeMode::default().id()}));
}
},
);
app.extra.show_ui_customization = open;
}