TASK 47 take four: alert ticker visible in the preview + 3 display methods
The creator confirmed the clip fix ("the video plays now"), then asked where the
scrolling text was — it was invisible, for a structural reason. AlertTickerFrame
existed only as a frame-pump callback blitted into the OUTPUT; PreviewPane.xaml had
no element for it, because the strip is master-width and global, not a Source, so it
cannot ride a per-element Image. Nothing was wrong in the renderer: there was no
consumer in the preview. Same class of defect as the missing IsAlertBox trigger, one
layer up (MyMistakes RULE 5/6).
- tickerPreviewSink on AlertOverlayLayer, published from RefreshAlertPreviews() so it
is always the UI thread; MainViewModel.AlertTicker writes it into one reused
WriteableBitmap bound to a new global AlertTickerElement, mirroring SocialBarElement.
- Source.AlertDisplayMethod + panel "Display" selector: TickerScroll / Flash / Solid.
Flash pulses 0.5s on / 0.5s off for the whole alert; Solid is centred and still.
- The marquee was also unreadable: a fixed 140px/s took ~17s per pass, so a 10s alert
showed the text once, entering from the right and never crossing. Paced in reads per
alert instead (TickerReadsPerAlert = 3 inside the alert's own length, speed derived
from it) — never px/s. Research (websearch: how do OBS/Streamlabs/StreamElements
alert boxes present announcement timing?) settled the unit: Streamlabs exposes "Alert
Duration: choose how long your alert stays on your stream" and "Text Delay", never a
scroll-speed slider (https://support.streamlabs.com/hc/en-us/articles/52499995174299-Setting-up-Your-Streamlabs-Alerts).
Run is phase-started half a frame in so the first frame isn't blank.
- Persistence: AlertDisplayMethod INTEGER NOT NULL DEFAULT 0 via the idempotent
table_info migration, appended LAST in the SELECT because the Source reader is
positional (GetInt32(32..34)) — a mid-list insert would silently shift a neighbour.
Tests: 15 new facts (suite 339/339). RealApp STA host: the pane draws the strip and
collapses at alert end; the layer publishes a real 1920x48 frame for all three methods
and nothing when the ticker is off; three passes counted in 10s by the pill's leading
edge resetting (a seamless marquee never blanks, so an empty frame cannot count a pass);
Solid byte-identical at every moment; Flash on for half of each second; the panel shows
and writes back the choice; the DB round-trips all three alert fields together.
Incidental finding: a bound ItemsSource ComboBox in LeftPanel.xaml broke
LayerReorderPersistenceTests.RealMouseDrag (that test injects PHYSICAL mouse input, so a
load-time re-measure moves the rows out from under the cursor). Rewritten as inline
ComboBoxItems, the shape the chat Font selector already uses in that panel. Recorded as
MyMistakes RULE (8).
This commit is contained in:
@@ -943,3 +943,49 @@ and the compositor resolver. The box was blank because `PreviewPane.xaml`'s per-
|
||||
the honest test asserts on the CONSUMER (is the Image Visible / are the canvas pixels
|
||||
different), never on the producer's own counters.
|
||||
- Corollary: never trust "the data arrived" as a root cause. Ask what draws it.
|
||||
|
||||
**RULE (6) — a per-element `Image` is the wrong home for a GLOBAL overlay, and an
|
||||
output-only producer is invisible by construction.** Same hunt, one layer up
|
||||
(2026-09-26, after the `IsAlertBox` fix): the creator confirmed the clip plays, then
|
||||
asked "where is the scrolling text? I do not see it." The ticker was healthy the whole
|
||||
time — `AlertTickerRenderer` produced a 1920x48 frame and `FramePump` blitted it at
|
||||
(0,0) — but it existed ONLY as a frame-pump callback (`alertTicker: () =>
|
||||
_alertLayer.AlertTickerFrame`). `PreviewPane.xaml` had no element for it, because the
|
||||
strip is master-width, not a `Source`, so it can't ride a per-element `Image`. Two
|
||||
lessons:
|
||||
- **When a feature's output is global (full frame width, a fixed overlay slot), it needs
|
||||
its own root element + its own VM-published `WriteableBitmap`, exactly like
|
||||
`SocialBarElement` — not a `Source`.** A global overlay that only a background thread
|
||||
consumes will reach the stream and never the creator's preview, and no amount of
|
||||
producer-side logging will show it: there is no consumer to be unhealthy.
|
||||
- **Ask "who displays this?" while designing, not after the report.** RULE (5) says
|
||||
audit the consumer when a picture is missing. RULE (6) is the earlier lesson: the
|
||||
consumer may not exist yet, and that is invisible from the producer side.
|
||||
|
||||
**RULE (7) — pace a marquee in READS PER ALERT, never in pixels per second.** The ticker
|
||||
shipped at a fixed `PixelsPerSecond = 140`, which sounds reasonable and was wrong: one
|
||||
full pass took ~17s, so the 10s default clip showed the announcement barely once,
|
||||
entering from the right and never crossing. The number that matches the creator's intent
|
||||
("readable three times") is **passes per alert**, divided into the alert's own length —
|
||||
the same convention the incumbent uses (Streamlabs exposes "Alert Duration: choose how
|
||||
long your alert stays on your stream" and "Text Delay", never a scroll-speed slider;
|
||||
https://support.streamlabs.com/hc/en-us/articles/52499995174299-Setting-up-Your-Streamlabs-Alerts).
|
||||
So: `readsPerAlert / alertSeconds` => a pass length => a speed derived from it. Corollary
|
||||
that cost a test rewrite: **a seamless marquee can never go blank**, so "the frame went
|
||||
empty" is NOT a valid way to count passes — count the pill's leading edge resetting to
|
||||
the right edge instead. And phase-start the run half a frame in, or the first published
|
||||
frame is blank (the strip sits exactly off the right edge) and the preview opens on
|
||||
nothing.
|
||||
|
||||
**RULE (8) — a bound `ItemsSource` ComboBox can break an unrelated real-mouse test.**
|
||||
`LeftPanel.xaml` gained a display selector written as
|
||||
`ItemsSource="{Binding ...}" + DisplayMemberPath + SelectedValuePath`; it compiled, my
|
||||
feature's tests passed, and it broke `LayerReorderPersistenceTests.RealMouseDrag_OnThe
|
||||
LayerList_PersistsTheReorder` — a test that injects PHYSICAL mouse input
|
||||
(`SetCursorPos` + `mouse_event`). A data-bound selection resolves during load and
|
||||
re-measures the panel under the test's feet, so the real cursor lands on the wrong row.
|
||||
Rewriting it as inline `<ComboBoxItem>`s (the shape the chat Font selector already uses
|
||||
in the same panel) fixed it with no other change. LESSON: **when a UI-only change breaks a
|
||||
real-input test, the control's data-binding style is the suspect, not the feature** — and
|
||||
bisect by reverting ONE file (`git checkout Controls/LeftPanel.xaml`) before theorising.
|
||||
Prefer the idiom already in the file you are editing.
|
||||
|
||||
Reference in New Issue
Block a user