Files
settled-reach/client/ui/implant/implant_app.gd
T
jpmschweitzerandClaude Opus 4.6 522116fb12 docs: PR #131 review — lifecycle ordering contract (item 9)
Documents the ImplantApp lifecycle ordering and the nav-stack state
guarantee at each hook:

- Class-level docstring on implant_app.gd describes on_install,
  on_open, on_close, and on_insert_deactivated: when each fires,
  what nav state subclasses can rely on, and what is safe to do
  (construct + register_screen in on_install; data refresh + read
  nav.current() in on_open; pause timers in on_close; no close_app
  manual call in on_insert_deactivated — call super or replicate
  the guard).

- Arch doc gains a "Lifecycle hooks" subsection under ImplantApp
  base class with a four-row contract table plus explanatory notes
  on two load-bearing invariants: why on_install sees an empty
  stack (bottom-up _ready order, no open signal yet); why on_close
  must not push/pop (would destroy preserved position on reopen).

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-04-19 15:26:31 +02:00

150 lines
4.4 KiB
GDScript

class_name ImplantApp
extends Control
## Base class for all implant apps. Absorbs HudGroups boilerplate; subclasses
## override lifecycle hooks only (#844, D-191).
##
## === Lifecycle ordering ===
##
## on_install() — called once from _ready(), after nav is created but BEFORE any
## HudGroups open event fires. The nav stack is EMPTY at this point. Use this
## hook to: construct screens and call register_screen(id, screen), set
## nav.set_default("..."), wire intra-screen signals. Do NOT rely on
## current_screen_id() here — no screen has been pushed yet.
##
## on_open(mode) — called every time HudGroups activates this app (FULLSCREEN or
## INSERT). By the time on_open fires, the base has ensured the nav stack is
## non-empty: if preserves_state=false, nav.reset_to_default() was called; if
## preserves_state=true and the stack was empty, nav.push_default() was called.
## nav.current() returns the visible screen id. Safe to read navigation state
## and trigger data refreshes here.
##
## on_close() — called every time HudGroups deactivates this app (GAMEPLAY mode
## or another app taking focus). Nav stack state is preserved here — do not
## push or pop screens in on_close. Use this hook for: pausing timers, stopping
## animations, unsubscribing from high-frequency feeds. The stack survives
## intact for the next on_open (if preserves_state=true).
##
## on_insert_deactivated() — called by SnapshotConsumers when the server drops
## insert state. The base implementation closes the app only if it is currently
## active in INSERT mode. FULLSCREEN apps inherit a no-op; override to add
## custom handling (e.g. save draft, emit warning). Do not call close_app()
## manually — call super() or replicate the guard condition.
##
## === Subclass _ready() pattern ===
## func _ready() -> void:
## manifest = load("res://ui/implant/apps/my_app/app.tres")
## super._ready()
## # additional init here if needed
signal app_opened(mode: int)
signal app_closed
var manifest: ImplantAppManifest = null
var nav: ImplantNavStack = null
var _screens: Dictionary = {} # screen_id → Control
var _current_screen_id: String = ""
func _ready() -> void:
set_anchors_preset(Control.PRESET_FULL_RECT)
mouse_filter = Control.MOUSE_FILTER_STOP
visible = false
if manifest:
HudGroups.register(self, manifest.app_path)
HudGroups.app_changed.connect(_internal_app_changed)
nav = ImplantNavStack.new()
nav.screen_changed.connect(_on_screen_changed)
add_child(nav)
on_install()
func _internal_app_changed(app_path: String, mode: int) -> void:
if manifest == null:
return
if app_path != manifest.app_path:
if visible:
visible = false
on_close()
app_closed.emit()
return
match mode:
HudGroups.Mode.FULLSCREEN, HudGroups.Mode.INSERT:
if not visible:
visible = true
if not manifest.preserves_state:
nav.reset_to_default()
elif nav.is_empty():
nav.push_default()
on_open(mode)
app_opened.emit(mode)
HudGroups.Mode.GAMEPLAY:
if visible:
visible = false
on_close()
app_closed.emit()
# --- Lifecycle hooks — subclasses override ---
func on_install() -> void:
pass
func on_open(_mode: int) -> void:
pass
func on_close() -> void:
pass
func on_insert_deactivated() -> void:
# Default: close only if active in INSERT mode. FULLSCREEN apps override to customize.
if manifest and HudGroups.is_app_active(manifest.app_path):
if HudGroups.get_active_mode() == HudGroups.Mode.INSERT:
HudGroups.close_app()
# --- Screen management ---
## Register a screen under an id. Base adds it as a child and starts it hidden.
## Call from on_install(). Wire signals before calling register_screen.
func register_screen(id: String, screen: Control) -> void:
if _screens.has(id):
push_warning("ImplantApp: screen id '%s' already registered" % id)
return
_screens[id] = screen
screen.visible = false
if screen.get_parent() == null:
add_child(screen)
func current_screen_id() -> String:
return _current_screen_id
func _on_screen_changed(new_id: String) -> void:
if _current_screen_id != new_id:
var old: Control = _screens.get(_current_screen_id, null)
if old:
if old.has_method("leave"):
old.leave()
old.visible = false
_current_screen_id = new_id
var s: Control = _screens.get(new_id, null)
if s:
s.visible = true
if s.has_method("enter"):
s.enter(nav.current_payload())
func handle_intent(_action: String, _params: Dictionary) -> void:
pass