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-box so 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.onScaleUpdated listener 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.