fix(display): end the scroll before the schedule-off blank and WiFi notice (#721)

The schedule-off blank and the WiFi notice are drawn by the display controller, not dispatched to a plugin, so #716's handover never reached them. Drawn while the last scroll's state was still set, the blank went out with the ticker's lagging rows on a scan-compensated panel and stayed up for its 60 s dwell, and the notice's redraws (which #712 now shows over a running scroller or Vegas) were timed as 0.5-1 s freezes and logged as a mid-scroll Render stall. The controller now calls set_scrolling_state(False) before drawing either; a scroller that resumes sets the state again on its next frame.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
Chuck
2026-10-01 20:46:01 -04:00
committed by GitHub
co-authored by Claude Opus 5.5
parent 0e78e06eb9
commit 85be4bf25d
4 changed files with 100 additions and 17 deletions
+63
View File
@@ -639,3 +639,66 @@ class TestRunLoop:
# Undecidable, so the broken screen's turn left the scroll state alone.
_, turn = _turn(c.events, "broken")
assert ("scrolling", False) not in turn
def _core_screen_controller(dm):
"""Just enough of a DisplayController to draw its own screens on ``dm``."""
from src.display_controller import DisplayController
c = MagicMock()
c.display_manager = dm
c.on_demand_active = False
c._end_scroll_before_core_screen = types.MethodType(
DisplayController._end_scroll_before_core_screen, c)
return c
def _record_scrolling(dm, monkeypatch):
"""The ``scrolling`` flag each presented frame is timed with."""
flags = []
real = dm.frame_timing.record
def record(*args, **kwargs):
flags.append(args[3])
return real(*args, **kwargs)
monkeypatch.setattr(dm.frame_timing, "record", record)
return flags
class TestTheControllersOwnScreens:
"""The schedule-off blank and the WiFi notice are drawn by the controller,
not dispatched to a plugin, so they end the scroll themselves."""
def test_the_schedule_off_blank_shows_none_of_the_ticker(self, dm, monkeypatch):
from src.display_controller import DisplayController
dm._scan_lag_bands = [(24, 48, 1)]
dm.set_scrolling_state(True, 1)
_push(dm, (255, 0, 0)) # the ticker's last frame
flags = _record_scrolling(dm, monkeypatch)
c = _core_screen_controller(dm)
DisplayController._blank_while_scheduled_off(c)
assert dm._presented[-1].getpixel((10, 30)) == (0, 0, 0)
assert flags == [False] # a static frame, not a freeze
assert not dm.is_currently_scrolling()
c._sleep_with_plugin_updates.assert_called_once_with(60)
def test_the_wifi_notice_shows_none_of_the_ticker(self, dm, monkeypatch):
from src.display_controller import DisplayController
dm._scan_lag_bands = [(24, 48, 1)]
dm.set_scrolling_state(True, 1)
_push(dm, (255, 0, 0))
flags = _record_scrolling(dm, monkeypatch)
c = _core_screen_controller(dm)
c._check_wifi_status_message.return_value = {"message": "x", "expires_at": 1e12}
c._display_wifi_status_message.side_effect = lambda _s: _push(dm, (0, 0, 255)) and True
assert DisplayController._show_wifi_notice(c) is True
assert dm._presented[-1].getpixel((10, 30)) == (0, 0, 255)
assert flags == [False]
def test_no_notice_leaves_the_scroll_alone(self, dm):
from src.display_controller import DisplayController
dm.set_scrolling_state(True, 2)
c = _core_screen_controller(dm)
c._check_wifi_status_message.return_value = None
assert DisplayController._show_wifi_notice(c) is False
assert dm.is_currently_scrolling()
assert dm._frame_hold == 2