Merge pull request #329 from grovebit/record-unarmed

Say at the Record button why Record did nothing
This commit is contained in:
Brandon Thomas authored and GitHub committed 2026-10-13 04:43:30 -04:00
commit 061c238e68
4 files changed
+80 -6

No files matched your search

+40 -1
View File
@@ -266,6 +266,8 @@ pub struct SoundApp {
pub(crate) docking_context: Option<egui::Context>,
/// Transport simulation when there is no player (tests, offscreen renders).
sim: Option<(Samples, Option<Samples>, Option<Range>)>,
/// When Record last found nothing to record on, and why: the Record buttons show it for a moment.
record_hint: Option<(f64, &'static str)>,
pub quit_requested: bool,
/// Reset floating-window positions next frame (Window › Arrange).
pub arrange_request: bool,
@@ -330,6 +332,7 @@ impl SoundApp {
theme_applied: None,
docking_context: None,
sim: None,
record_hint: None,
quit_requested: false,
arrange_request: false,
touched: std::collections::HashSet::new(),
@@ -383,6 +386,13 @@ impl SoundApp {
self.engine.transport.playing
}
/// Why the last Record press did nothing, for a few seconds and while it still holds.
pub(crate) fn record_hint(&self) -> Option<&'static str> {
let (at, why) = self.record_hint?;
let fresh = self.last_frame.unwrap_or_default() - at < 4.0;
(fresh && !self.engine.session().tracks.iter().any(|t| t.mixer.record_arm)).then_some(why)
}
/// Current transport position (playhead while playing, else the edit insertion).
pub fn position(&self) -> Samples {
if self.engine.transport.playing { self.engine.transport.position } else { self.engine.session().edit.selection.start }
@@ -450,7 +460,13 @@ impl SoundApp {
fn start_recording(&mut self) {
let armed = self.engine.session().tracks.iter().any(|t| t.mixer.record_arm);
if !armed {
self.ui.status = "Record-enable a track first (the red button in its header).".into();
let why = if self.engine.session().tracks.is_empty() {
"Nothing to record on yet: add a track with Track › New…, then click its R button."
} else {
"No track is record-enabled: click the R button on the track to record."
};
self.ui.status = why.into();
self.record_hint = Some((self.last_frame.unwrap_or_default(), why));
return;
}
self.ensure_input();
@@ -1021,6 +1037,29 @@ mod tests {
assert_eq!(app.engine.transport.position, 0);
}
#[test]
fn record_with_nothing_armed_says_what_to_do() {
let mut app = SoundApp::new(soundcraft_engine::Engine::default(), None, Services::default());
app.run("transport.record", json!({})).unwrap();
app.handle_transport(0.25);
assert!(!app.is_playing() && !app.engine.transport.recording);
assert!(app.record_hint().is_some_and(|why| why.contains("Track › New")), "{:?}", app.record_hint());
app.run("track.new", json!({"count": 1, "format": "mono"})).unwrap();
app.run("transport.record", json!({})).unwrap();
app.handle_transport(0.25);
assert!(app.record_hint().is_some_and(|why| why.contains("R button")), "{:?}", app.record_hint());
assert_eq!(Some(app.ui.status.as_str()), app.record_hint());
app.last_frame = Some(5.0);
assert_eq!(app.record_hint(), None, "the hint shows for a few seconds");
app.last_frame = None;
app.run("transport.record", json!({})).unwrap();
app.handle_transport(0.25);
app.run("mix.record_arm", json!({})).unwrap();
assert_eq!(app.record_hint(), None, "arming a track answers the hint");
}
#[test]
fn a_hidden_window_still_autosaves() {
let dir = std::env::temp_dir().join(format!("soundcraft-hidden-autosave-{}", std::process::id()));
+7 -3
View File
@@ -272,15 +272,19 @@ fn transport_window(app: &mut SoundApp, ctx: &egui::Context) {
("end", "transport.go_to_end"),
("record", "transport.record"),
] {
if crate::widgets::icon_button(
let resp = crate::widgets::icon_button(
ui,
vec2(34.0, 26.0),
icon,
(cmd == "transport.play" && app.is_playing()) || (cmd == "transport.record" && app.engine.transport.recording),
cmd,
)
.clicked()
);
if cmd == "transport.record"
&& let Some(why) = app.record_hint()
{
egui::Tooltip::for_widget(&resp).show(|ui| ui.label(why));
}
if resp.clicked() {
let _ = app.run(cmd, json!({}));
}
}
+8 -2
View File
@@ -283,7 +283,7 @@ fn transport(app: &mut SoundApp, ui: &mut Ui) {
let playing = app.is_playing();
let recording = app.engine.transport.recording;
let looped = app.engine.session().edit.loop_playback;
group(ui, 176.0, 64.0, |ui, _| {
group(ui, 176.0, 64.0, |ui, g| {
ui.vertical(|ui| {
ui.spacing_mut().item_spacing = vec2(3.0, 4.0);
ui.horizontal(|ui| {
@@ -306,7 +306,13 @@ fn transport(app: &mut SoundApp, ui: &mut Ui) {
let (r, resp) = ui.allocate_exact_size(vec2(38.0, 28.0), Sense::click());
key(ui.painter(), r, &resp, if recording { Color32::from_rgb(110, 30, 30) } else { key_fill(&resp) });
icons::draw(ui.painter(), r.shrink(5.0), "record", t.rec);
if resp.on_hover_text("Record (Cmd+Space)").clicked() {
let resp = resp.on_hover_text("Record (Cmd+Space)");
if let Some(why) = app.record_hint() {
// Below the whole group, so the hint covers none of its buttons.
let below = Rect::from_min_max(r.min, pos2(r.max.x, g.max.y));
egui::Tooltip::always_open(ui.ctx().clone(), resp.layer_id, resp.id, below).show(|ui| ui.label(why));
}
if resp.clicked() {
let _ = app.run("transport.record", json!({}));
}
});
+25
View File
@@ -0,0 +1,25 @@
//! Record with nothing to record on explains itself at the Record buttons.
use egui::vec2;
use egui_kittest::Harness;
use egui_kittest::kittest::Queryable;
use soundcraft_ui_egui::{Services, SoundApp};
#[test]
fn record_in_an_empty_session_shows_why_at_both_record_buttons() {
let mut app = SoundApp::new(soundcraft_engine::Engine::default(), None, Services::default());
app.ui.show_transport = true;
let mut h = Harness::builder().with_size(vec2(1600.0, 1000.0)).build_ui_state(
|ui, app: &mut SoundApp| {
app.logic(&ui.ctx().clone());
app.ui(ui);
},
app,
);
h.run_steps(5);
assert_eq!(h.query_all_by_label_contains("Nothing to record on").count(), 0);
h.get_by_label("transport.record").click();
h.run_steps(3);
assert!(!h.state().engine.transport.recording);
assert_eq!(h.query_all_by_label_contains("Nothing to record on").count(), 2, "the toolbar's and the Transport window's");
}