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:
@@ -479,6 +479,16 @@
|
|||||||
ItemsSource="{Binding ScoreboardEdgeOptions}"
|
ItemsSource="{Binding ScoreboardEdgeOptions}"
|
||||||
SelectedItem="{Binding ScoreboardEdge, Mode=TwoWay}"/>
|
SelectedItem="{Binding ScoreboardEdge, Mode=TwoWay}"/>
|
||||||
|
|
||||||
|
<DockPanel Margin="10,0,0,6">
|
||||||
|
<TextBlock DockPanel.Dock="Left" Text="Scroll speed" Foreground="{StaticResource Brush.OnSurfaceVariant}"
|
||||||
|
FontSize="11" Width="76" VerticalAlignment="Center"/>
|
||||||
|
<TextBlock DockPanel.Dock="Right" Width="56" TextAlignment="Right" FontSize="11"
|
||||||
|
Foreground="{StaticResource Brush.OnSurfaceVariant}" VerticalAlignment="Center"
|
||||||
|
Text="{Binding ScoreboardScrollSpeed, StringFormat={}{0:0} px/s}"/>
|
||||||
|
<Slider Minimum="20" Maximum="400" SmallChange="10" LargeChange="40"
|
||||||
|
Value="{Binding ScoreboardScrollSpeed, Mode=TwoWay}" VerticalAlignment="Center"/>
|
||||||
|
</DockPanel>
|
||||||
|
|
||||||
<TextBlock Text="Facts" Foreground="{StaticResource Brush.OnSurfaceVariant}" FontSize="11" FontWeight="SemiBold" Margin="0,0,0,4"/>
|
<TextBlock Text="Facts" Foreground="{StaticResource Brush.OnSurfaceVariant}" FontSize="11" FontWeight="SemiBold" Margin="0,0,0,4"/>
|
||||||
<WrapPanel Margin="10,0,0,0">
|
<WrapPanel Margin="10,0,0,0">
|
||||||
<CheckBox Style="{StaticResource YtCheckBox}" Content="Subscribers" Margin="0,0,10,4"
|
<CheckBox Style="{StaticResource YtCheckBox}" Content="Subscribers" Margin="0,0,10,4"
|
||||||
|
|||||||
@@ -31,13 +31,21 @@ public sealed class ScoreboardPresenter
|
|||||||
/// creator previews is what ships.</summary>
|
/// creator previews is what ships.</summary>
|
||||||
public bool Scroll { get; set; } = true;
|
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
|
/// <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
|
/// measured run, cropped to a moving window, so a tick is a row-wise byte copy rather than a
|
||||||
/// re-rasterisation.</summary>
|
/// re-rasterisation.</summary>
|
||||||
public void Advance(double seconds, double pixelsPerSecond = 120)
|
public void Advance(double seconds, double? pixelsPerSecond = null)
|
||||||
{
|
{
|
||||||
if (!Scroll) return;
|
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>
|
/// <summary>The strip frame for the current tallies, or null when nothing is enabled/shown.</summary>
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
using Microsoft.Data.Sqlite;
|
using Microsoft.Data.Sqlite;
|
||||||
|
using ytLive.Services.Compositor;
|
||||||
using ytLive.Models;
|
using ytLive.Models;
|
||||||
|
|
||||||
namespace ytLive.Services;
|
namespace ytLive.Services;
|
||||||
@@ -57,6 +58,15 @@ public partial class LayoutStore : IDisposable
|
|||||||
|
|
||||||
public void SaveScoreboardEnabled(bool enabled) => UpsertSetting("Scoreboard.Enabled", enabled ? "1" : "0");
|
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
|
/// <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>
|
/// default <c>%APPDATA%\ytLlive\recordings</c>. Restored at startup.</summary>
|
||||||
public string? LoadRecordFolder()
|
public string? LoadRecordFolder()
|
||||||
|
|||||||
@@ -43,6 +43,20 @@ public partial class MainViewModel
|
|||||||
}
|
}
|
||||||
|
|
||||||
public IReadOnlyList<BarEdge> ScoreboardEdgeOptions { get; } = new[] { BarEdge.Top, BarEdge.Bottom };
|
public IReadOnlyList<BarEdge> ScoreboardEdgeOptions { get; } = new[] { BarEdge.Top, BarEdge.Bottom };
|
||||||
|
|
||||||
|
/// <summary>How fast the ticker travels, in pixels per second. Persisted like the rest of the
|
||||||
|
/// strip's look: it is a readability choice, and re-dialling it every launch would be a tax.</summary>
|
||||||
|
public double ScoreboardScrollSpeed
|
||||||
|
{
|
||||||
|
get => _scoreboardPresenter?.PixelsPerSecond ?? ScoreboardPresenter.DefaultPixelsPerSecond;
|
||||||
|
set
|
||||||
|
{
|
||||||
|
var clamped = Math.Clamp(value, ScoreboardPresenter.MinPixelsPerSecond, ScoreboardPresenter.MaxPixelsPerSecond);
|
||||||
|
if (_scoreboardPresenter != null) _scoreboardPresenter.PixelsPerSecond = clamped;
|
||||||
|
_layoutStore.SaveScoreboardScrollSpeed((int)Math.Round(clamped));
|
||||||
|
OnPropertyChanged(nameof(ScoreboardScrollSpeed));
|
||||||
|
}
|
||||||
|
}
|
||||||
private Source? _scoreboardPreviewSource;
|
private Source? _scoreboardPreviewSource;
|
||||||
|
|
||||||
internal ScoreboardRecorder ScoreboardRecorder => _scoreboard;
|
internal ScoreboardRecorder ScoreboardRecorder => _scoreboard;
|
||||||
|
|||||||
@@ -280,6 +280,7 @@ public partial class MainViewModel : ViewModelBase
|
|||||||
_musicPlayer.UserGain = (float)Helpers.GainDb.FromDb(TraxDefaultDb); // background by default
|
_musicPlayer.UserGain = (float)Helpers.GainDb.FromDb(TraxDefaultDb); // background by default
|
||||||
InitScoreboard();
|
InitScoreboard();
|
||||||
ScoreboardEnabled = _layoutStore.LoadScoreboardEnabled();
|
ScoreboardEnabled = _layoutStore.LoadScoreboardEnabled();
|
||||||
|
ScoreboardScrollSpeed = _layoutStore.LoadScoreboardScrollSpeed();
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
var s = _layoutStore.GetSetting("ZoomShape");
|
var s = _layoutStore.GetSetting("ZoomShape");
|
||||||
|
|||||||
@@ -48,6 +48,16 @@ public class ScoreboardPresenterTests
|
|||||||
Assert.NotNull(ticked);
|
Assert.NotNull(ticked);
|
||||||
Assert.NotEqual(first.BgraPixels, ticked!.BgraPixels);
|
Assert.NotEqual(first.BgraPixels, ticked!.BgraPixels);
|
||||||
|
|
||||||
|
// SPEED is a real dial: the same elapsed time at twice the speed moves twice as far,
|
||||||
|
// so the window lands somewhere else.
|
||||||
|
var slow = new ScoreboardPresenter(recorder, new ScoreboardStyle()) { PixelsPerSecond = 60 };
|
||||||
|
var fast = new ScoreboardPresenter(recorder, new ScoreboardStyle()) { PixelsPerSecond = 120 };
|
||||||
|
slow.SetTiles(tiles);
|
||||||
|
fast.SetTiles(tiles);
|
||||||
|
slow.Advance(1.0);
|
||||||
|
fast.Advance(1.0);
|
||||||
|
Assert.NotEqual(slow.Frame!.BgraPixels, fast.Frame!.BgraPixels);
|
||||||
|
|
||||||
// A visible change re-rasterises the run.
|
// A visible change re-rasterises the run.
|
||||||
recorder.Record(Member("Second"));
|
recorder.Record(Member("Second"));
|
||||||
var second = presenter.Frame;
|
var second = presenter.Frame;
|
||||||
|
|||||||
@@ -29,6 +29,11 @@ public class ScoreboardPropertiesTests
|
|||||||
Assert.Contains("Simulate (dev)", block);
|
Assert.Contains("Simulate (dev)", block);
|
||||||
Assert.Contains("Brush.Attention", block);
|
Assert.Contains("Brush.Attention", block);
|
||||||
|
|
||||||
|
// Scroll speed: a real dial, persisted, with its range owned by the presenter.
|
||||||
|
Assert.Contains("ScoreboardScrollSpeed", block);
|
||||||
|
Assert.Contains("Minimum=\"20\" Maximum=\"400\"", block);
|
||||||
|
Assert.Contains("Scoreboard.ScrollSpeed", SourceAssert.Text("Services", "LayoutStore.Settings.cs"));
|
||||||
|
|
||||||
// The master switch sits in the same panel: off hides the strip and stops the polling.
|
// The master switch sits in the same panel: off hides the strip and stops the polling.
|
||||||
Assert.Contains("ScoreboardEnabled", panel);
|
Assert.Contains("ScoreboardEnabled", panel);
|
||||||
|
|
||||||
@@ -61,7 +66,7 @@ public class ScoreboardPropertiesTests
|
|||||||
{
|
{
|
||||||
"ScoreboardTextColor", "ScoreboardBackgroundColor", "ScoreboardBackgroundOpacity",
|
"ScoreboardTextColor", "ScoreboardBackgroundColor", "ScoreboardBackgroundOpacity",
|
||||||
"ScoreboardFontSize", "ScoreboardColorOptions", "ScoreboardShowSubscribers",
|
"ScoreboardFontSize", "ScoreboardColorOptions", "ScoreboardShowSubscribers",
|
||||||
"ScoreboardEnabled", "CanUseScoreboard",
|
"ScoreboardEnabled", "CanUseScoreboard", "ScoreboardScrollSpeed",
|
||||||
})
|
})
|
||||||
Assert.NotNull(type.GetProperty(name));
|
Assert.NotNull(type.GetProperty(name));
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user