@@ -346,6 +346,96 @@ pub fn set_trail_config(enabled: bool, ttl_ms: u32, diffuse: bool, teleport_thre
346346 save_prefs ( ) ;
347347}
348348
349+ // ── toggles (host keybinds: flip current state, return the new one) ──────────
350+ //
351+ // These exist so the mpv shim can bind a key to "flip" a control and show the
352+ // resulting state in the OSD, without keeping its own mirror that could drift
353+ // from Studio's OSC pushes. Each flips the *current* value atomically and
354+ // returns the new one; persistence mirrors the matching `set_*` (enable / labels
355+ // / trails persist; objects / heatmap / bands / colormap are transient, owned by
356+ // Studio and re-pushed on connect).
357+
358+ /// Flip the master enable and return the new state. Persisted.
359+ pub fn toggle_enabled ( ) -> bool {
360+ let o = overlay ( ) ;
361+ let new = !o. enabled . load ( Ordering :: Relaxed ) ;
362+ o. enabled . store ( new, Ordering :: Relaxed ) ;
363+ save_prefs ( ) ;
364+ new
365+ }
366+
367+ /// Flip object-label visibility and return the new state. Persisted.
368+ pub fn toggle_labels ( ) -> bool {
369+ let new = {
370+ let Ok ( mut s) = overlay ( ) . state . lock ( ) else {
371+ return false ;
372+ } ;
373+ s. labels_enabled = !s. labels_enabled ;
374+ s. labels_enabled
375+ } ;
376+ save_prefs ( ) ;
377+ new
378+ }
379+
380+ /// Flip object visibility (markers + labels + trails + depth lines) and return
381+ /// the new state. Transient (display-only), like [`set_objects_visible`].
382+ pub fn toggle_objects ( ) -> bool {
383+ let Ok ( mut s) = overlay ( ) . state . lock ( ) else {
384+ return false ;
385+ } ;
386+ s. objects_visible = !s. objects_visible ;
387+ s. objects_visible
388+ }
389+
390+ /// Flip whether the trails are drawn and return the new state. Clears the trail
391+ /// buffers when disabling (so they don't reappear on re-enable). Persisted, like
392+ /// [`set_trail_config`].
393+ pub fn toggle_trails ( ) -> bool {
394+ let new = {
395+ let Ok ( mut s) = overlay ( ) . state . lock ( ) else {
396+ return false ;
397+ } ;
398+ s. cfg . enabled = !s. cfg . enabled ;
399+ if !s. cfg . enabled {
400+ s. trails . clear ( ) ;
401+ }
402+ s. cfg . enabled
403+ } ;
404+ save_prefs ( ) ;
405+ new
406+ }
407+
408+ /// Flip the energy heatmap and return the new state. Transient, like
409+ /// [`set_heatmap_enabled`].
410+ pub fn toggle_heatmap ( ) -> bool {
411+ let Ok ( mut s) = overlay ( ) . state . lock ( ) else {
412+ return false ;
413+ } ;
414+ s. heatmap_enabled = !s. heatmap_enabled ;
415+ s. heatmap_enabled
416+ }
417+
418+ /// Advance the heatmap colour gradient to the next index (wraps 0..=4, mirroring
419+ /// `OBJECT_ENERGY_COLORMAPS`) and return the new index. Transient.
420+ pub fn cycle_heatmap_colormap ( ) -> usize {
421+ let Ok ( mut s) = overlay ( ) . state . lock ( ) else {
422+ return 0 ;
423+ } ;
424+ s. heatmap_colormap = ( s. heatmap_colormap + 1 ) % 5 ;
425+ s. heatmap_colormap as usize
426+ }
427+
428+ /// Step the heatmap depth-plane count by `delta` (clamped to 1..=12) and return
429+ /// the new count. Transient, like [`set_heatmap_bands`].
430+ pub fn adjust_heatmap_bands ( delta : i32 ) -> usize {
431+ let Ok ( mut s) = overlay ( ) . state . lock ( ) else {
432+ return FIELD_BANDS ;
433+ } ;
434+ let next = ( s. heatmap_bands as i32 + delta) . clamp ( 1 , 12 ) as usize ;
435+ s. heatmap_bands = next;
436+ next
437+ }
438+
349439// ── persistence (orender-owned, real-time, separate from the savable config) ─
350440
351441/// Point the overlay at its prefs file and load it. Called once by the host at
@@ -1259,6 +1349,47 @@ mod tests {
12591349 assert ! ( build_ass( 0 , 0 ) . is_empty( ) ) ;
12601350 }
12611351
1352+ #[ test]
1353+ fn toggles_flip_and_return_new_state ( ) {
1354+ let _g = guard ( ) ;
1355+ // guard() leaves enable/labels/objects/heatmap on; each toggle flips and
1356+ // reports the resulting value.
1357+ assert ! ( !toggle_enabled( ) ) ;
1358+ assert ! ( toggle_enabled( ) ) ;
1359+ assert ! ( !toggle_labels( ) ) ;
1360+ assert ! ( toggle_labels( ) ) ;
1361+ assert ! ( !toggle_objects( ) ) ;
1362+ assert ! ( toggle_objects( ) ) ;
1363+ assert ! ( !toggle_heatmap( ) ) ;
1364+ assert ! ( toggle_heatmap( ) ) ;
1365+ }
1366+
1367+ #[ test]
1368+ fn toggle_trails_clears_buffers_when_disabling ( ) {
1369+ let _g = guard ( ) ;
1370+ // Build up a trail, then disable: the buffer must be dropped so it can't
1371+ // reappear on re-enable.
1372+ update_positions ( vec ! [ ( 0 , 0.0 , 0.0 , 0.5 , String :: new( ) ) ] ) ;
1373+ let now = now_secs ( ) ;
1374+ trail_append ( & mut overlay ( ) . state . lock ( ) . unwrap ( ) , 0 , 0.0 , 0.0 , 0.5 , now) ;
1375+ assert ! ( !overlay( ) . state. lock( ) . unwrap( ) . trails. is_empty( ) ) ;
1376+ assert ! ( !toggle_trails( ) , "trails start enabled (default) → off" ) ;
1377+ assert ! ( overlay( ) . state. lock( ) . unwrap( ) . trails. is_empty( ) ) ;
1378+ assert ! ( toggle_trails( ) , "back on" ) ;
1379+ }
1380+
1381+ #[ test]
1382+ fn heatmap_bands_clamp_and_colormap_wraps ( ) {
1383+ let _g = guard ( ) ;
1384+ set_heatmap_bands ( 3 ) ;
1385+ assert_eq ! ( adjust_heatmap_bands( 2 ) , 5 ) ;
1386+ assert_eq ! ( adjust_heatmap_bands( -10 ) , 1 , "clamps at 1" ) ;
1387+ assert_eq ! ( adjust_heatmap_bands( 100 ) , 12 , "clamps at 12" ) ;
1388+ set_heatmap_colormap ( 3 ) ;
1389+ assert_eq ! ( cycle_heatmap_colormap( ) , 4 ) ;
1390+ assert_eq ! ( cycle_heatmap_colormap( ) , 0 , "wraps 4 → 0" ) ;
1391+ }
1392+
12621393 // The energy heatmap is now a separate BGRA bitmap (drawn under the ASS via
12631394 // mpv's overlay-add), not part of the ASS string.
12641395 #[ test]
0 commit comments