popout/state: implement Navigating + AwaitingContent + double-load fix
NavigateRequested in any media state (DisplayingImage / LoadingVideo / PlayingVideo / SeekingVideo) transitions to AwaitingContent and emits [StopMedia, EmitNavigate]. NavigateRequested in AwaitingContent itself (rapid Right-arrow spam, second nav before main_window has delivered the next post) emits EmitNavigate alone — no StopMedia, because there's nothing to stop. This is the structural fix for the double-load race that 31d02d3c fixed upstream by removing the explicit _on_post_activated call after _grid._select. The popout-layer fix is independent and stronger: even if upstream signal chains misfire, the state machine never produces two Load effects for the same navigation cycle. The state machine's LoadVideo / LoadImage effects only fire from ContentArrived, and ContentArrived is delivered exactly once per main_window-side post activation. The Open event handler also lands here. Stashes saved_geo, saved_fullscreen, monitor on the state machine instance for the first ContentArrived to consume. The actual viewport seeding from saved_geo lives in commit 8 — this commit just stores the inputs. Tests passing after this commit (62 total → 42 pass, 20 fail): - test_awaiting_open_stashes_saved_geo - test_awaiting_navigate_emits_navigate_only - test_displaying_image_navigate_stops_and_emits - test_loading_video_navigate_stops_and_emits - test_playing_video_navigate_stops_and_emits - test_seeking_video_navigate_stops_and_emits - test_invariant_double_navigate_no_double_load (RACE FIX!) Plus several illegal-transition cases for nav-from-now-valid-states. Phase A (16 tests in tests/core/) still green. Tests still failing (20, scheduled for commits 6-11): - SeekingVideo entry/exit (commit 6) - F11 round-trip (commit 7) - Persistent viewport / drift events (commit 8) - mute/volume/loop persistence events (commit 9) - DisplayingImage content arrived branch (commit 10) - Closing transitions (commit 10) Test cases for commit 6 (SeekingVideo + slider pin): - PlayingVideo + SeekRequested → SeekingVideo + SeekVideoTo effect - SeekingVideo + SeekRequested replaces seek_target_ms - SeekingVideo + SeekCompleted → PlayingVideo - test_invariant_seek_pin_uses_compute_slider_display_ms
This commit is contained in:
parent
7fdc67c613
commit
a9ce01e6c1
@ -609,9 +609,22 @@ class StateMachine:
|
|||||||
# ------------------------------------------------------------------
|
# ------------------------------------------------------------------
|
||||||
|
|
||||||
def _on_open(self, event: Open) -> list[Effect]:
|
def _on_open(self, event: Open) -> list[Effect]:
|
||||||
# Real implementation: stash saved_geo / saved_fullscreen /
|
"""Initial popout-open event from the adapter.
|
||||||
# monitor on self for the first ContentArrived to consume.
|
|
||||||
# Lands in commit 5.
|
Stashes the cross-popout-session class-level state
|
||||||
|
(`_saved_geometry`, `_saved_fullscreen`, the chosen monitor)
|
||||||
|
on the state machine instance for the first ContentArrived
|
||||||
|
handler to consume. After Open the machine is still in
|
||||||
|
AwaitingContent — the actual viewport seeding from saved_geo
|
||||||
|
happens inside the first ContentArrived (commit 8 wires the
|
||||||
|
actual viewport math; this commit just stashes the inputs).
|
||||||
|
|
||||||
|
No effects: the popout window is already constructed and
|
||||||
|
showing. The first content load triggers the first fit.
|
||||||
|
"""
|
||||||
|
self.saved_geo = event.saved_geo
|
||||||
|
self.saved_fullscreen = event.saved_fullscreen
|
||||||
|
self.monitor = event.monitor
|
||||||
return []
|
return []
|
||||||
|
|
||||||
def _on_content_arrived(self, event: ContentArrived) -> list[Effect]:
|
def _on_content_arrived(self, event: ContentArrived) -> list[Effect]:
|
||||||
@ -652,9 +665,34 @@ class StateMachine:
|
|||||||
return []
|
return []
|
||||||
|
|
||||||
def _on_navigate_requested(self, event: NavigateRequested) -> list[Effect]:
|
def _on_navigate_requested(self, event: NavigateRequested) -> list[Effect]:
|
||||||
# Real implementation: emits StopMedia + EmitNavigate,
|
"""**Double-load race fix (replaces 31d02d3c's upstream signal-
|
||||||
# transitions to AwaitingContent. Lands in commit 5.
|
chain trust fix at the popout layer).**
|
||||||
return []
|
|
||||||
|
From a media-bearing state (DisplayingImage / LoadingVideo /
|
||||||
|
PlayingVideo / SeekingVideo): transition to AwaitingContent
|
||||||
|
and emit `[StopMedia, EmitNavigate]`. The StopMedia clears the
|
||||||
|
current surface so mpv doesn't keep playing the previous video
|
||||||
|
during the async download wait. The EmitNavigate tells
|
||||||
|
main_window to advance selection and eventually deliver the
|
||||||
|
new content via ContentArrived.
|
||||||
|
|
||||||
|
From AwaitingContent itself (rapid Right-arrow spam, second
|
||||||
|
nav before main_window has delivered): emit EmitNavigate
|
||||||
|
ALONE — no StopMedia, because there's nothing to stop. The
|
||||||
|
state stays AwaitingContent. **The state machine never
|
||||||
|
produces two LoadVideo / LoadImage effects for the same
|
||||||
|
navigation cycle, no matter how many NavigateRequested events
|
||||||
|
the user fires off.** That structural property is what makes
|
||||||
|
the eof race impossible at the popout layer.
|
||||||
|
"""
|
||||||
|
if self.state == State.AWAITING_CONTENT:
|
||||||
|
return [EmitNavigate(direction=event.direction)]
|
||||||
|
# Media-bearing state: clear current media + emit nav
|
||||||
|
self.state = State.AWAITING_CONTENT
|
||||||
|
return [
|
||||||
|
StopMedia(),
|
||||||
|
EmitNavigate(direction=event.direction),
|
||||||
|
]
|
||||||
|
|
||||||
def _on_video_started(self, event: VideoStarted) -> list[Effect]:
|
def _on_video_started(self, event: VideoStarted) -> list[Effect]:
|
||||||
"""LoadingVideo → PlayingVideo. Persistence effects fire here.
|
"""LoadingVideo → PlayingVideo. Persistence effects fire here.
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user