Standalone Gameface overlays

A floating modern panel can work over both the hangar and the hybrid battle HUD. The robust shape is a Gameface ViewImpl inside one non-fullscreen, panel-sized Wulf WindowImpl on a context-appropriate layer, parented to the current loaded main window.

  • Use one native window for the complete panel, not one window per label or value.
  • Use WindowFlags.WINDOW, not a fullscreen surface.
  • Choose the lowest window layer that stays above the panel's owning UI.
  • Pass the current Wulf main window as the explicit parent.
  • Publish the real surface size from Gameface before relying on placement or drag bounds.

Window layer, focus, and keyboard routing

A Wulf layer is part of the input contract, not only the drawing order. A persistent passive panel should remain above its owning page but below native menus and dialogs. Verified clients through 2.4 use WindowLayer.WINDOW for passive garage and battle panels and show(False) from its native _onReady callback so readiness does not take keyboard focus.

class GaragePanelWindow(WindowImpl):
    def __init__(self, content, parent):
        super(GaragePanelWindow, self).__init__(
            WindowFlags.WINDOW,
            content=content,
            layer=WindowLayer.WINDOW,
            parent=parent,
        )

    def _onReady(self):
        self.show(False)

The native lobby menu is a higher top window. A passive garage panel placed on WindowLayer.OVERLAY can sit above that menu and prevent a second Escape press from closing it even when the panel did not take focus at startup. show(False) and the correct layer solve different problems: the first controls readiness-time focus; the second preserves native stacking and keyboard routing.

Prevent the first-click requirement in battle

If a battle starts but keyboard input does not behave normally until the player clicks inside the client, check whether the standalone Wulf window took focus as it became ready. Put show(False) on the native WindowImpl._onReady callback—not on the contained ViewImpl—so displaying the overlay preserves battle input focus.

from frameworks.wulf import WindowFlags, WindowLayer
from gui.impl.pub import WindowImpl


class BattleOverlayWindow(WindowImpl):
    def __init__(self, content, parent):
        super(BattleOverlayWindow, self).__init__(
            WindowFlags.WINDOW,
            content=content,
            layer=WindowLayer.WINDOW,
            parent=parent,
        )

    def _onReady(self):
        # Show the panel without taking keyboard focus from the battle.
        self.show(False)

WindowLayer.WINDOW remains above the battle page while leaving native menus and queued popup windows above the passive panel. Because the TAB statistics surface belongs to the battle page beneath that layer, hide the panel explicitly during FULL_STATS, FULL_STATS_QUEST_PROGRESS, FULL_STATS_PERSONAL_RESERVES, and EVENT_STATS while ctx['isDown'] is true.

Temporary hide versus destruction

Current Wulf source defines show(self, focus=True) and hide(self, destroy=False). The argument to hide is a destruction flag, not a focus flag. Use hide(False) for a temporary passive-panel hide, restore its saved position if necessary, and use show(False) to reveal it without taking focus. Do not move a window offscreen as a hide mechanism; the window area can clamp it back into view.

Multiple standalone surfaces

A single Python mod file can own more than one standalone Gameface surface, but each surface needs its own identity, parent owner, model lifetime, size contract, and teardown path.

  • Use one generation token per surface type or per owning context.
  • Keep lobby and battle parent references separate.
  • Destroy only the surface you own; do not close a shared parent window.
  • Initialize each model with explicit default values before sending incremental updates.
  • Make repeated creation converge to one live surface per owner instead of appending duplicates.
_surfaces = {}

def remember_surface(key, surface):
    old = _surfaces.get(key)
    if old is not None and old is not surface:
        destroy_surface(old)
    _surfaces[key] = surface

def clear_surface(key, surface):
    if _surfaces.get(key) is surface:
        _surfaces.pop(key, None)

Python view and model contract

Hand-written view models do not receive generated property setters. Registering storage with _addStringProperty is not enough; define the matching setter using the same property index.

class OverlayModel(ViewModel):
    def __init__(self, payload):
        self.__payload = payload
        super(OverlayModel, self).__init__(properties=1, commands=1)

    def _initialize(self):
        super(OverlayModel, self)._initialize()
        self._addStringProperty('payload', self.__payload)
        self.onReady = self._addCommand('onReady')

    def setPayload(self, value):
        self._setString(0, value)


class OverlayView(ViewImpl):
    def __init__(self, layout_id, initial_payload):
        self.__model = OverlayModel(initial_payload)
        super(OverlayView, self).__init__(layout_id)

    @property
    def viewModel(self):
        return self.__model

    def updatePayload(self, payload):
        with self.__model.transaction() as model:
            model.setPayload(payload)

Without the explicit setter, a transaction call such as model.setPayload(...) raises AttributeError. Generated native models hide this detail because their accessors are generated in advance.

Initial value and later updates

  • Put the complete initial payload into the model constructor.
  • Avoid publishing transactions from the earliest loading phase.
  • Let JavaScript confirm engine, DOM, and sizing readiness through a command.
  • Publish later updates through a model transaction after readiness.
  • Keep data acquisition separate from UI rendering so a frozen value is diagnosed at the correct layer.

Payload design

JSON is a practical bridge for a compact panel with several related values. Use explicit fields and explicit DOM targets instead of anonymous positional spans.

{
  "title": "Session",
  "primaryValue": "1,842",
  "secondaryValue": "+4.7%",
  "secondaryColor": "#67c97a",
  "status": "Live"
}
function render(payloadText) {
  const data = JSON.parse(payloadText || '{}');
  document.getElementById('panel-title').textContent = data.title || '';
  document.getElementById('primary-value').textContent = data.primaryValue || '—';

  const secondary = document.getElementById('secondary-value');
  secondary.textContent = data.secondaryValue || '';
  secondary.style.color = data.secondaryColor || '';

  document.getElementById('status-value').textContent = data.status || '';
}
  • Send presentation colors as ordinary CSS strings such as #rrggbb or rgba(...).
  • Apply color only to the semantic value element; keep labels and separators neutral.
  • Keep literal labels and punctuation in HTML or localization resources rather than embedding HTML in Python strings.
  • Validate payload size and types before publishing them.

Gameface data updates

After readiness, listen to the view environment's data-change event and also perform an initial render. The initial model may predate the listener.

engine, viewEnv, window.model, and window.subViews are runtime globals injected by Gameface and the World of Tanks integration. They do not come from an installed JavaScript package. Check their current behavior in a loaded client view and shipped frontend bundles rather than inferring it from repository dependencies or typings.

const ROOT_MODEL_RES_ID = 0;
let callbackId = null;

function renderCurrentModel() {
  render(window.model.payload);
}

engine.whenReady.then(() => {
  engine.on('viewEnv.onDataChanged', renderCurrentModel);
  callbackId = viewEnv.addDataChangedCallback(
    'model',
    ROOT_MODEL_RES_ID,
    true
  );
  renderCurrentModel();
});

Remove the engine listener and release callbackId with viewEnv.removeDataChangedCallback(callbackId, ROOT_MODEL_RES_ID) when the view is torn down. For an attached child model, read window.subViews.get(resId).model and use that same resource ID for registration and removal.

A short JavaScript poll can be a temporary compatibility fallback, but it should not replace the model event without evidence. When a battle value remains frozen, verify the Python event, controller cache, or feedback source before changing the Gameface bridge. Periodic reconciliation from an authoritative cached summary can recover when one event or hook was missed.

Collections and derived host state

Updating a model collection may not update host properties such as visibility, item count, or version tokens. Follow the native presenter path: mutate within the model transaction, update required derived fields, and emit the host’s normal update signal.

For multi-surface native data, see Models & Native Projections.

Shared runtime for multiple panels

When one feature owns multiple standalone panels, treat them as several surfaces of one runtime rather than several independent mini-mods. The battle data, subscriptions, toggles, and diagnostics should be shared; each native panel owns only its window, model, readiness, drag state, and teardown.

  • Give each panel kind its own Wulf window, model instance, readiness flag, generation token, saved position, and native name.
  • Use one shared runtime to publish updates to every loaded panel model.
  • Use one layout entry only when the same HTML bundle can render every panel mode from the payload.
  • Use separate layout entries when markup, dependencies, sizing, or readiness differ.
  • Do not simulate independent dragging by moving DOM nodes inside one oversized transparent Gameface surface.
  • Persist panel positions under panel-specific keys and write after drag release.

Parent-window transition hazards

A loaded main window is necessary, but heavy lobby/battle transitions can still be unsafe while native reconstruction is unwinding. A delayed create path must re-check the current player, route, arena period, loaded parent, and generation token immediately before load().

  • Resolve the current main window on every retry instead of reusing a cached parent.
  • Cancel delayed creation on context changes, disconnect, replay seek, and final teardown.
  • Log success only after load() is actually called against the current loaded parent.
  • Use short guarded delays only to avoid a verified unsafe reconstruction window, not as a replacement for lifecycle checks.

Make native visibility calls idempotent

Repeatedly requesting a window's existing visibility can emit Wulf errors on every update. Keep page, window and content readiness gates, then query the current state with Window.isHidden(). A cached desired-visible flag cannot detect native visibility changes made elsewhere.

def apply_visibility(window, visible):
    # Call only after the owning window and content are ready.
    hidden = window.isHidden()
    if visible and hidden:
        window.show(False)
    elif not visible and not hidden:
        window.hide(False)

hide(False) retains the window; teardown still destroys it and cancels callbacks. This guard does not establish readiness or choose a window layer. Recheck the native proxy contract after client updates.

Surface and command ownership checklist

Group content that moves together in one panel-sized native surface and model. Independently movable or independently hidden panels can use separate native surfaces backed by one runtime snapshot. Do not simulate independent drag ownership by moving nodes within a larger hidden hit-test rectangle.

Track native-window readiness and frontend/DOM/post-resize readiness separately. Apply saved position only after the window has a native area and actual size. Hiding a retained window does not destroy its document or stop its handlers; destruction and owner teardown must release those resources. For a full communication contract, see Communication and Updates.