feat(scoreboard): scroll speed is a property (20-400 px/s, persisted)

The marquee now has the dial it needs. `ScoreboardPresenter.PixelsPerSecond` is the single owner of
the range (20-400, default 120), the layer's properties expose it with a live px/s readout, and the
value is persisted as Scoreboard.ScrollSpeed because it is a READABILITY choice — re-dialling it
every launch would be a tax on the creator who needs the strip to be readable.

Advance() still takes an optional override so tests can drive exact distances.

Guard: the presenter test proves speed is real (same elapsed time, twice the speed, different
window) — not a value that is stored and ignored — and the panel test pins the control, its range
and the persistence key.
This commit is contained in:
2026-10-30 15:19:28 -07:00
parent 460b36204d
commit 28525c90aa
7 changed files with 61 additions and 3 deletions
+10 -2
View File
@@ -31,13 +31,21 @@ public sealed class ScoreboardPresenter
/// creator previews is what ships.</summary>
public bool Scroll { get; set; } = true;
/// <summary>Marquee speed in pixels per second — the creator's dial (a read-speed choice, not a
/// styling one).</summary>
public double PixelsPerSecond { get; set; } = DefaultPixelsPerSecond;
public const double DefaultPixelsPerSecond = 120;
public const double MinPixelsPerSecond = 20;
public const double MaxPixelsPerSecond = 400;
/// <summary>Advances the scroll by <paramref name="seconds"/>. The strip is a marquee: one
/// measured run, cropped to a moving window, so a tick is a row-wise byte copy rather than a
/// re-rasterisation.</summary>
public void Advance(double seconds, double pixelsPerSecond = 120)
public void Advance(double seconds, double? pixelsPerSecond = null)
{
if (!Scroll) return;
_offsetPx += seconds * pixelsPerSecond;
_offsetPx += seconds * (pixelsPerSecond ?? PixelsPerSecond);
}
/// <summary>The strip frame for the current tallies, or null when nothing is enabled/shown.</summary>
+10
View File
@@ -1,4 +1,5 @@
using Microsoft.Data.Sqlite;
using ytLive.Services.Compositor;
using ytLive.Models;
namespace ytLive.Services;
@@ -57,6 +58,15 @@ public partial class LayoutStore : IDisposable
public void SaveScoreboardEnabled(bool enabled) => UpsertSetting("Scoreboard.Enabled", enabled ? "1" : "0");
/// <summary>Scoreboard marquee speed (px/s), restored at startup.</summary>
public int LoadScoreboardScrollSpeed()
=> int.TryParse(GetSetting("Scoreboard.ScrollSpeed"), out var px)
? Math.Clamp(px, (int)ScoreboardPresenter.MinPixelsPerSecond, (int)ScoreboardPresenter.MaxPixelsPerSecond)
: (int)ScoreboardPresenter.DefaultPixelsPerSecond;
public void SaveScoreboardScrollSpeed(int pixelsPerSecond)
=> UpsertSetting("Scoreboard.ScrollSpeed", pixelsPerSecond.ToString());
/// <summary>The user's chosen record output folder (TASK 18), or null to use the
/// default <c>%APPDATA%\ytLlive\recordings</c>. Restored at startup.</summary>
public string? LoadRecordFolder()