Say at the Record button why Record did nothing

Fixes #316: in a new session, clicking Record did nothing visible. With
no record-enabled track there is nothing to record, and the reason went
only to the end of the dim status line at the bottom of the Edit
window. In a new session it also pointed at "the red button in its
header", which no track existed to show.

- Record with nothing to record on says what to do: add a track with
  Track › New… when the session has none, else click R on the track
  to record.
- The message shows for 4 seconds below the toolbar's Record button
  (under the whole transport group, so it covers none of its buttons)
  and under the Transport window's Record button, as well as in the
  status line. It goes early once a track is record-enabled.
- Nothing is armed or created on the user's behalf, and the transport
  stays stopped.
This commit is contained in:
grovebit 2026-10-12 17:47:27 +01:00
parent 6cf896e76c
commit 0a5fc6ccba
4 changed files with 80 additions and 6 deletions

View File

@ -259,6 +259,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,
@ -318,6 +320,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(),
@ -369,6 +372,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 }
@ -435,7 +445,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();
@ -998,6 +1014,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()));

View File

@ -198,15 +198,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!({}));
}
}

View File

@ -275,7 +275,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| {
@ -328,7 +328,13 @@ fn transport(app: &mut SoundApp, ui: &mut Ui) {
StrokeKind::Inside,
);
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!({}));
}
});

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");
}