popout/state: implement DisplayingImage + Closing — all 62 tests pass
Two final transition handlers complete the state machine surface:
ContentArrived(IMAGE | GIF) in any state →
DisplayingImage, [LoadImage(path, is_gif), FitWindowToContent(w, h)]
Same path as the video branch but routes to ImageViewer instead
of mpv. The is_gif flag tells the adapter which ImageViewer
method to call (set_gif vs set_image — current code at
popout/window.py:411-417).
CloseRequested from any non-Closing state →
Closing, [StopMedia, EmitClosed]
Closing is terminal. Every subsequent event returns [] regardless
of type (handled at the dispatch entry, which has been in place
since the skeleton). The adapter handles geometry persistence and
Qt cleanup outside the state machine — those are not popout
state machine concerns.
Tests passing after this commit: 62 / 62 (100%).
Newly green:
- test_awaiting_content_arrived_image_loads_and_transitions
- test_awaiting_content_arrived_gif_loads_as_animated
- test_displaying_image_content_replace_with_image
- test_close_from_each_state_transitions_to_closing[5 states]
Phase A (16 tests in tests/core/) still green.
State machine complete. The 6-state / 17-event / 14-effect design
is fully implemented:
States (6, budget ≤10):
AwaitingContent / DisplayingImage / LoadingVideo /
PlayingVideo / SeekingVideo / Closing
Events (17, budget ≤20):
Open / ContentArrived / NavigateRequested
VideoStarted / VideoEofReached / VideoSizeKnown
SeekRequested / SeekCompleted
MuteToggleRequested / VolumeSet / LoopModeSet / TogglePlayRequested
FullscreenToggled
WindowMoved / WindowResized / HyprlandDriftDetected
CloseRequested
Effects (14, budget ≤15):
LoadImage / LoadVideo / StopMedia
ApplyMute / ApplyVolume / ApplyLoopMode
SeekVideoTo / TogglePlay
FitWindowToContent / EnterFullscreen / ExitFullscreen
EmitNavigate / EmitPlayNextRequested / EmitClosed
Six race-fix invariants all enforced structurally — no timestamp
suppression windows in state.py, no guards, no fall-throughs:
1. EOF race: VideoEofReached only valid in PlayingVideo
2. Double-load: Navigate from media → AwaitingContent never
re-emits Load until ContentArrived
3. Persistent viewport: viewport is a state field, only mutated
by user-action events
4. F11 round-trip: pre_fullscreen_viewport snapshot/restore
5. Seek pin: SeekingVideo state + compute_slider_display_ms read
6. Pending mute: state.mute owned by machine, ApplyMute on
PlayingVideo entry
Test cases for commit 11 (illegal transition handler):
- dispatch invalid event in strict mode raises InvalidTransition
- dispatch invalid event in release mode returns [] (current behavior)
- BOORU_VIEWER_STRICT_STATE env var gates the raise
This commit is contained in:
parent
527cb3489b
commit
4fb17151b1
@ -661,8 +661,21 @@ class StateMachine:
|
|||||||
content_h=event.height,
|
content_h=event.height,
|
||||||
),
|
),
|
||||||
]
|
]
|
||||||
# Image / GIF lands in commit 10 (DisplayingImage transitions).
|
# Image or GIF: transition straight to DisplayingImage and
|
||||||
return []
|
# emit LoadImage. The is_gif flag tells the adapter which
|
||||||
|
# ImageViewer method to call (set_gif vs set_image).
|
||||||
|
self.is_first_content_load = False
|
||||||
|
self.state = State.DISPLAYING_IMAGE
|
||||||
|
return [
|
||||||
|
LoadImage(
|
||||||
|
path=event.path,
|
||||||
|
is_gif=(event.kind == MediaKind.GIF),
|
||||||
|
),
|
||||||
|
FitWindowToContent(
|
||||||
|
content_w=event.width,
|
||||||
|
content_h=event.height,
|
||||||
|
),
|
||||||
|
]
|
||||||
|
|
||||||
def _on_navigate_requested(self, event: NavigateRequested) -> list[Effect]:
|
def _on_navigate_requested(self, event: NavigateRequested) -> list[Effect]:
|
||||||
"""**Double-load race fix (replaces 31d02d3c's upstream signal-
|
"""**Double-load race fix (replaces 31d02d3c's upstream signal-
|
||||||
@ -1002,9 +1015,22 @@ class StateMachine:
|
|||||||
return []
|
return []
|
||||||
|
|
||||||
def _on_close_requested(self, event: CloseRequested) -> list[Effect]:
|
def _on_close_requested(self, event: CloseRequested) -> list[Effect]:
|
||||||
# Real implementation: transitions to Closing, emits StopMedia
|
"""Esc / Q / X / closeEvent. Transition to Closing from any
|
||||||
# + EmitClosed. Lands in commit 10.
|
non-Closing source state. Closing is terminal — every
|
||||||
|
subsequent event returns [] regardless of type (handled at
|
||||||
|
the dispatch entry).
|
||||||
|
|
||||||
|
Entry effects: StopMedia (clear current surface) + EmitClosed
|
||||||
|
(tell main_window the popout is closing). The adapter is
|
||||||
|
responsible for any cleanup beyond clearing media — closing
|
||||||
|
the Qt window, removing the event filter, persisting
|
||||||
|
geometry to the class-level _saved_geometry — but those are
|
||||||
|
adapter-side concerns, not state machine concerns.
|
||||||
|
"""
|
||||||
|
if self.state == State.CLOSING:
|
||||||
return []
|
return []
|
||||||
|
self.state = State.CLOSING
|
||||||
|
return [StopMedia(), EmitClosed()]
|
||||||
|
|
||||||
|
|
||||||
__all__ = [
|
__all__ = [
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user