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
#rrggbborrgba(...). - 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.
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.