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 ## T-971 (AtlasAgentInterface, D-226 item 4): a generic, read-only screen ## lookup by id — the same `_screens` dict register_screen() already ## maintains, exposed so an agent-control channel can reach a specific ## screen instance (StepCanvasViewer via RegionalScreen.get_viewer(), etc.) ## without every ImplantApp subclass growing its own bespoke getters. Returns ## null for an unregistered id, matching Dictionary.get()'s own default. func get_screen(screen_id: String) -> Variant: return _screens.get(screen_id, null) 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