Fixed and dynamic panel dimensions
Prefer fixed dimensions for compact overlays unless dynamic sizing has been verified in the
current client. Measuring an auto or inline-block panel inside a zero-
or one-pixel native surface creates a circular dependency: the content wraps to the tiny
surface, so the measured width remains tiny.
.feature-panel {
width: 360px;
height: 116px;
box-sizing: border-box;
overflow: hidden;
}
.feature-row {
height: 28px;
display: flex;
flex-flow: row nowrap;
align-items: center;
overflow: hidden;
}
.feature-row > span {
flex: 0 0 auto;
white-space: nowrap;
}
- Use the same authored width and height in CSS, initial Python expectations, and
resizeViewPx. - Use
border-boxso borders remain inside the declared dimensions. - Prevent independently colored value rows from wrapping onto the following row.
- For genuine dynamic sizing, measure independently of the current containing block and update Python hit testing after every resize.
Measure after the current render
Reading offsetWidth, offsetHeight, or a bounding rectangle in the same
call that rebuilds the DOM can return geometry from the previous layout pass. A verified
three-line panel stayed one line high until a later update forced another measurement.
let contentGeneration = 0;
function renderAndResize(data) {
const generation = ++contentGeneration;
render(data);
requestAnimationFrame(() => requestAnimationFrame(() => {
if (generation !== contentGeneration) return;
const rect = panel.getBoundingClientRect();
viewEnv.resizeViewPx(Math.ceil(rect.width), Math.ceil(rect.height));
}));
}
Two animation frames are an observed working boundary, not a universal guarantee. Keep the generation check so a delayed measurement cannot resize the native surface for content that has already changed.
Creating the Wulf window
The window needs a real, current parent. Resolve the GUI loader, obtain windowsManager.getMainWindow(),
and wait until that main window has a live proxy and WindowStatus.LOADED.
def create_overlay_window(layout_id, payload):
gui_loader = dependency.instance(IGuiLoader)
main_window = gui_loader.windowsManager.getMainWindow()
if main_window is None:
return None
if main_window.windowStatus != WindowStatus.LOADED:
return None
view = OverlayView(layout_id, payload)
window = WindowImpl(
content=view,
parent=main_window,
layer=WindowLayer.WINDOW,
flags=WindowFlags.WINDOW
)
window.load()
return window
Do not call window.move() immediately after load(). Before the window
joins an area, its ID, layer, or content may still be undefined. Store the requested position,
wait for the JavaScript ready command after resizing, then move once.
Scaling
Treat authored panel dimensions as the scale-1 baseline. Read the Gameface scale after engine and DOM readiness, scale the complete panel from the top-left, and resize the native surface to the same scaled dimensions.
const BASE_WIDTH = 360;
const BASE_HEIGHT = 116;
const panel = document.querySelector('.feature-panel');
function applyScale() {
const scale = viewEnv.getScale();
panel.style.transformOrigin = '0 0';
panel.style.transform = `scale(${scale})`;
viewEnv.resizeViewPx(
Math.ceil(BASE_WIDTH * scale),
Math.ceil(BASE_HEIGHT * scale)
);
}
engine.whenReady.then(() => {
applyScale();
engine.on('self.onScaleUpdated', applyScale);
});
- Do not scale only the font or only the native surface.
- Python drag hit testing and clamping should use the loaded window's actual positive
window.size. - After a scale update changes the surface size, notify Python so anchored placement can be reapplied.
- Use authored dimensions only as a pre-readiness fallback.
- Remove the
self.onScaleUpdatedlistener with the same function object during teardown.
Physical pixels and Wulf coordinates
Global cursor and screen APIs do not always use the same coordinate space as a Wulf window.
GUI.screenResolution() and the normalized value from
GUI.mcursor().position can describe physical screen pixels, while window.position,
window.size, and window.move() can use the scaled coordinate space of
the loaded Wulf main window.
Do not assume the two spaces are identical. Derive the conversion from the current loaded main window and its positive size, then keep each calculation in one space:
- Convert the normalized global cursor to physical pixels for stable drag tracking and screen-relative persistence.
- Convert the loaded Wulf window position and size to physical pixels before hit testing or clamping against the physical screen.
- Convert the final physical target back into Wulf coordinates immediately before calling
window.move(). - Recalculate the conversion after resolution, UI-scale, or main-window changes.
Dragging and placement
Use stable global cursor coordinates and the window's start position. Measuring motion in coordinates local to a window that is itself moving creates feedback and drift.
def begin_drag(cursor_x, cursor_y, window_x, window_y):
_drag.start_cursor = (cursor_x, cursor_y)
_drag.start_window = (window_x, window_y)
_drag.active = True
def update_drag(cursor_x, cursor_y, screen_w, screen_h, window_w, window_h):
if not _drag.active:
return
dx = cursor_x - _drag.start_cursor[0]
dy = cursor_y - _drag.start_cursor[1]
target_x = max(0, min(screen_w - window_w, _drag.start_window[0] + dx))
target_y = max(0, min(screen_h - window_h, _drag.start_window[1] + dy))
move_window_if_changed(target_x, target_y)
- Center or anchor once after the post-resize ready command confirms the real size.
- Deduplicate moves so periodic data refreshes do not call the same coordinates repeatedly.
- Persist normalized or anchor-relative coordinates when resolution and scale changes must be supported.
- Keep drag input bound only while the panel needs it and release held state during context transitions.