Commit Graph
11 Commits
Author SHA1 Message Date
gramps c03acd0aa0 fix(scoreboard): the defaults were never applied — a record struct zero-initialises
Creator's defaults: white text, Background ON (black, 20%), speed capped at 200 px/s, text size shown
in real pixels, and "plate" dropped from the label.

The important find is underneath those: `ScoreboardStyle` is a record STRUCT, and `new
ScoreboardStyle()` zero-initialises — it does NOT run the constructor's parameter defaults. So the
shipped strip had null text colour, NO background and zero opacity, whatever the source said. That is
why the Background checkbox and the opacity slider looked like they had no code behind them: the
renderer was faithfully drawing the zeros it was handed.

An explicit parameterless constructor now carries the real defaults, and the renderer test asserts
them (white / on / black / 0.20 / max speed 200) so this class of silent-zero cannot return.

Readouts fixed too: the opacity and text-size setters never raised their own property, so the numbers
never moved while the render did. Text size now reads "24 px" instead of a bare slider value, and
snaps to whole pixels.
2026-10-30 15:24:14 -07:00
gramps 28525c90aa 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.
2026-10-30 15:19:28 -07:00
gramps 460b36204d feat(scoreboard): it ticks — the marquee is the design, not an option
Creator, plainly: "the entire point of a ticker is to tick… so, tick." The static strip was my
reading of an earlier exchange; it is gone.

How it ticks without becoming the compositor's most expensive layer:
- `ScoreboardRenderer.RenderRun` rasterises the run ONCE per value change at its natural width plus
  a gap.
- `ScoreboardPresenter` crops a 1920-wide window at a moving offset each tick — a row-wise byte
  copy, no text layout, no re-raster.
- `TickScoreboard` runs on the existing 30 Hz meter clock and only while the strip is shown, so a
  hidden or disabled scoreboard costs nothing.

The dev simulation uses the same path, so what is previewed is what ships.

Test updated to the real contract: no tick → identical pixels; a tick → different pixels; a value
change → re-rasterised run. (The old test asserted frame-instance identity, which a cropped window
can never satisfy.)
2026-10-30 15:17:55 -07:00
gramps 882b8551d9 fix(scoreboard): the strip and the social bar now really share the frame
Reported: the ticker was painting OVER the socials. The coupling rule existed and was tested
(ScoreboardLayout.ResolveEdges) but nothing ever CALLED it — the strip's Y still came from its own
preference, so both bars landed on the south edge.

Now the resolution runs at publish time: the social bar keeps its side, the scoreboard takes the
other one, and its Y follows from that edge. The pair re-sorts whenever the social bar moves (the
preview click-toggle) or shows/hides, because the rule is "resolve from what is visible", not a
stored pairing.

The layer also gains an Edge preference (Top/Bottom) in its properties, labelled "socials take the
other" — a preference, never a claim on an occupied edge.

Also stabilised the opacity assertion: it samples a plate-only pixel 5px from the left edge instead
of the first semi-transparent pixel it finds (which could be anti-aliased text).
2026-10-30 15:11:35 -07:00
gramps e27e79962c fix(scoreboard): box follows the rendered strip, opacity proven, plate always settable, dev override
Four reports, three causes:

1. "Background plate has no code behind it" — the plate COLOUR and OPACITY controls were disabled
   until the plate switch was on, which reads as dead controls. They are always settable now; the
   switch is what decides whether a plate is drawn.
2. "Opacity doesn't work" — proven in the renderer test: 20% and 80% produce alphas of ~51 and ~204,
   so the slider reaches the pixels. What was actually wrong was the box.
3. The strip's BOX was fixed at 56px while a wrapped strip renders taller — the frame was scaled
   into the box and looked squashed/clipped. The source's width/height now follow the rendered
   frame, and its Y follows from the edge.
4. "Simulate (dev) should override Show the scoreboard" — it does: the preview gate is
   (_scoreboardEnabled || _scoreboardSimulate), so the dev toggle works even with the product
   setting off.

Not changed: the strip is STATIC by design (all enabled facts always visible, wrap rather than
scroll). If scrolling is wanted after all, that is a presentation mode to add, not a fix.
2026-10-30 15:09:19 -07:00
gramps 9405c54750 feat(scoreboard): dev mode can configure the board without signing in
Creator (2026-10-30): in dev mode the "Show the scoreboard" option must always be available.

The block's gate becomes CanUseScoreboard = IsConnected || IsDevelopmentBuild. Signed-out Release
still shows the inert block with its "sign in" line; a DEBUG build is fully usable, so the strip can
be toggled and previewed before there is an account. IsConnected raises it, so signing in flips it
live.

Guard: ScoreboardPropertiesTests now asserts the CanUseScoreboard gate (enabled AND hint), and the
VM surface list includes it.
2026-10-30 15:01:32 -07:00
gramps e6abc13461 feat(scoreboard): a DEBUG-only "Simulate" switch, so the strip is provable in the preview
Creator (2026-10-30): pre-1.0, use DEBUG — add a toggle that simulates the ticker so the strip can
be SEEN and its compositing verified before a real stream.

The sample travels the REAL ScoreboardRecorder a stream uses (one new member, a gift of three, a
milestone, a $12.34 Super Chat), so what the preview shows is exactly what live events produce — a
fake renderer would prove nothing. Toggling off clears to an empty session, so the empty state is
visible too.

Debug-gated at COMPILE time: `DevelopmentBuild` is a const true in DEBUG / false in Release, and the
checkbox binds its Visibility to it. A shipped Release build therefore has no dev control at all,
rather than one hidden by a runtime flag.

Guard: ScoreboardSimulationTests — the sample lands all four tallies through the recorder, off clears,
and the panel + const pair exist in the Dual (DEBUG/Release) form.
2026-10-30 14:58:56 -07:00
gramps 38c2ad6555 feat(scoreboard): a master on/off switch that also stops the polling
Creator: "even if it's not visible, we want to avoid polling."

ScoreboardEnabled is the layer's master switch in the properties panel. Off means two things:
- the strip frame is null in every scene (per-scene eye toggles stay independent), and
- the cadence policy returns None at every phase — a scoreboard nobody can see must not spend quota.

The switch is persisted (Scoreboard.Enabled), because one that resets would quietly resume polling.

The polling gate is ScoreboardPollsEnabled: master switch AND at least one scene with a visible
scoreboard — so hiding it in all five scenes also stops the polls.

Guards: ScoreboardCadenceTests adds the switch off across live/test/recording; ScoreboardProperties
pins the checkbox and the VM surface.
2026-10-30 14:54:05 -07:00
gramps 85a4a8a7d3 feat(scoreboard): the layer is available to ALL scenes (YT_ACTIVITY_TICKER)
Creator ruling: the Scoreboard layer exists in every scene rather than being added five times.
ScoreboardHealer.Ensure keeps exactly one per scene — healed on load like the Background and Brand
logo — so a scene can never end up with two, and 'off' is the ordinary per-scene eye toggle rather
than a delete.

It is appended as the TOPMOST layer on purpose: a full-width strip draws over the scene, and
inserting it above the background would break the Brand logo's documented invariant (directly above
the background). That choice is also why the layer-reorder test still passes untouched.

Also: a layer-list icon for the type, so the row is not blank.

Guard: ScoreboardHealerTests — one per scene, topmost, exact strip geometry, idempotent, and a
duplicate healed back to one.
2026-10-30 14:50:20 -07:00
gramps 534f56a7c4 feat(scoreboard): units 7+8 — the layer's properties panel (facts, colours, plate, size)
Selecting a Scoreboard layer now shows its settings, using the app's own widgets only:
- eight fact checkboxes (subscribers, watch hours, Super Chats, new members, milestones, last member,
  last Super Chat, last gift)
- text colour and plate colour via YtComboBox over BorderColorPalette (the app's one palette —
  TASK 63's rule: a picker IS the app's drop-down, never a hand-built menu)
- plate on/off, plate opacity, text size 14-24

Every change pushes through the presenter (SetStyle/SetTiles) and repaints the strip, so the preview
and the output follow immediately. One look per layout, stored on the view-model: the scoreboard is a
single billboard, not eight independent widgets.

Guard: ScoreboardPropertiesTests asserts the panel is gated on SelectedElement.IsScoreboard, binds
all eight facts and the four look controls, uses YtComboBox/YtCheckBox only, and that the VM surface
exists for each name (a typo would otherwise bind to nothing in silence).
2026-10-30 14:41:31 -07:00
gramps deb74529a6 feat(scoreboard): unit 6 — it is a LAYER (source type, strip geometry, resolver, session hook)
Adds SourceType.Scoreboard with the same per-scene eye toggle, drag and presence as every other
element — 'prominent on BRB, dialled back on Live' is geometry, not a second mechanism. Default
geometry is the strip: 1920 wide, 56 tall, bottom edge.

The output resolver returns the presenter's cached frame exactly like the chat box and alert box do,
and every session start clears the tallies. Preview publication writes one shared WriteableBitmap on
the UI thread (the pump thread must never touch one), refreshed only when a displayed value moves.

Guard: ScoreboardLayerTests pins the enum member, the geometry constants, the resolver case and the
session hook, so the layer cannot quietly become a special widget.
2026-10-30 14:40:31 -07:00