Skip to content

ui

FreeBodyEngine.ui #

UIElement(tag=None, styles={}) #

Bases: GenericElement

Generic UI element.

Supported styles:

Sizing

width height

Padding

padding padding_left padding_right padding_top padding_bottom

Child layout

layout gap

Positioning

parent_anchor anchor x y

Text

text String to draw over the element via the engine's MSDF text pipeline. Empty (the default) draws nothing.

font Path to a raw font file (e.g. "test.ttf"), resolved the same way any other asset path is - relative to the project's asset directory. Its MSDF atlas is generated and cached the first time it's used (see core/files/loaders/font.py's resolve_font()); no separate build step or .fbfont asset is required.

font_size Text size in pixels.

font_weight Selects a weight variant of font - either a name ("thin", "extralight", "light", "regular", "medium", "semibold", "bold", "extrabold", "black", plus the aliases "normal"/"book"/"heavy"/"demibold") or a CSS-style number 100-900. Default is "regular"/400 (no change from font as given).

Resolved as a sibling file next to `font` following the
"{family}-{Weight}.ttf" naming convention (e.g. "bold" with
font="JetBrainsMono-Regular.ttf" looks for
"JetBrainsMono-Bold.ttf" beside it) - see
core/files/loaders/font.py's resolve_font(). This selects a
real, separately-authored font file; it doesn't synthesize
bold/faux-bold from a single weight. If no matching file
exists, warns once and falls back to `font` unchanged.

text_color (r, g, b, a) text color, 0-1 per channel.

State overrides

normal clicked hover selected

Args: tag: Optional identifier for this element, not used by the layout/draw code itself. styles: This element's styles - see the style documentation above.

DEFAULT_STYLES = {'width': 0, 'height': 0, 'padding': 0, 'layout': 'vertical', 'gap': 0, 'x': 0, 'y': 0, 'parent_anchor': 'center', 'anchor': 'bottom_left', 'base_color': (0.0, 0.0, 0.0, 0.0), 'border_radius': 0, 'border_width': 0, 'border_color': (0.0, 0.0, 0.0, 1.0), 'image': None, 'text': '', 'font': None, 'font_size': 24, 'font_weight': 'regular', 'text_color': (1.0, 1.0, 1.0, 1.0), 'editable': False, 'secret': False, 'scroll': False, 'overflow': 'visible', 'scrollbar_width': 8, 'scrollbar_margin': 2, 'scrollbar_min_thumb': 24, 'scrollbar_track': {}, 'scrollbar_thumb': {}} class-attribute instance-attribute #

VALID_ANCHORS = {'top_left', 'top_center', 'top_right', 'center_left', 'center', 'center_right', 'bottom_left', 'bottom_center', 'bottom_right'} class-attribute instance-attribute #

VALID_OVERFLOWS = {'visible', 'hidden', 'scroll', 'auto'} class-attribute instance-attribute #

animations = [] instance-attribute #

id = uuid.uuid4() instance-attribute #

parent instance-attribute #

state = ElementStates.NORMAL instance-attribute #

styles = styles instance-attribute #

calculate_layout(root, parent_layout=None) #

Calculate this element's size and position, then recursively calculate the layout of its children.

clear(event) #

Removes every callback registered for event via on() - for re-rendering a element whose behavior changes with some state (a follow button toggling between "Follow"/"Unfollow", say) without accumulating a duplicate callback on it every time.

get_current_styles() #

Merge base styles with state-specific styles.

Example:

{
    "width": 100,

    "hover": {
        "width": 120
    }
}

When hovered, width becomes 120.

get_overflow(styles=None) #

Resolves the effective "overflow" mode - "visible", "hidden", "scroll", or "auto" - taking the "scroll": True legacy alias into account (see the OVERFLOW docs above). Checked via _has_own_style rather than the merged dict for the same reason anchor detection is: DEFAULT_STYLES always contains "overflow": "visible", so a plain presence check could never tell "explicitly set" apart from "just the default" - and an explicit "overflow" always wins over the older "scroll" flag if an element somehow has both.

get_style(name) #

Gets the raw, un-merged value of style name set directly on this element (not through get_current_styles() - so no DEFAULT_STYLES fallback and no state-override merging), warning if it isn't set.

off(event, callback) #

Unregisters callback from event, if it was registered.

on(event, callback) #

Registers callback to run when event fires on this element - "hover_enter", "hover_exit", "press", "release", "click", or "submit" (editable elements only) - see the INTERACTION style docs above. Multiple callbacks may be registered for the same event.

scroll_by(delta_px) #

Adjusts this element's scroll offset by delta_px (only has any visible effect if this element's "scroll" style is on). Clamped to the valid range on the next layout pass, so over-scrolling here just settles back to the nearest edge next frame rather than needing to be clamped here against content it hasn't measured yet.

set_state(state) #

Transition this element to a new interaction state (normal, hover, clicked, selected). This is the only place self.state should be assigned from outside the class - input handling (mouse-over/click detection against self._layout) should call this rather than setting element.state directly, so behavior stays consistent if this method grows validation/callbacks later.

set_style(name, val, duration=0, curve=Linear) #

Set or animate a style.

Immediate:

element.set_style(
    "width",
    200
)

Animated:

element.set_style(
    "width",
    200,
    duration=0.5
)

UIManager(styles={}) #

Bases: Service

Engine service owning the UI tree's root element - the entry point for adding/removing top-level UI elements, and for driving their layout/ animation update plus mouse hit-testing, keyboard text-input routing, and wheel scrolling (see the INTERACTION style docs in ui/element.py).

Args: styles: Root-level styles, sized to the current framebuffer.

SCROLL_SPEED = 40.0 class-attribute instance-attribute #

root = RootElement(win_size[0], win_size[1], styles) instance-attribute #

add(element) #

Adds element as a top-level child of the UI root.

draw() #

Draws the UI tree.

on_destroy() #

Unregisters the callbacks registered in on_initialize.

on_initialize() #

Registers the resize/draw/update callbacks this service needs while active.

paste_text(pasted) #

Inserts pasted into the focused field at the cursor, or does nothing if there's no focused field or pasted is empty/None. Factored out of _on_key's own Ctrl+V handling so a backend that can't answer Window.get_clipboard_text() synchronously (see WebWindow's own docstring on exactly this - the browser Clipboard API is async, this engine's clipboard contract isn't) can still support pasting by calling this directly once it does have the text in hand, from wherever it actually got it (WebWindow does this from the browser's native paste DOM event instead, which - unlike navigator.clipboard.readText() - hands over clipboard contents synchronously as part of the event itself).

remove(element) #

Removes element from the UI root's children.

resize(size) #

Resizes the root layout area to match the new framebuffer size.

update() #

Advances any running style animations, recalculates the whole tree's layout from the (possibly now-changed) styles, then runs mouse hit-testing/hover/click/scroll for this frame.

UIRenderer() #

Bases: Service

Engine service that draws the UI tree owned by the ui service (UIManager).

Loads the shared background quad mesh/material used to draw every element.

material = load_file('engine://ui/element.fbmat') instance-attribute #

quad = generate_quad() instance-attribute #

draw() #

Draws every top-level element of the UI tree (and, recursively, their children).

Depth testing is explicitly turned off first, then back on after - nothing here ever did this before, so the UI silently inherited whatever depth-test state the 3D pipeline (PBRPipeline) happened to leave behind (on for its opaque/lighting passes - see graphics/pbr/pipeline.py - off only during its own composite step). Every UI element's background and text share the same mesh, drawn at the same implicit depth, in the same frame's depth buffer as whatever 3D content came before - with depth testing left on and GL's default depth func (GL_LESS), a background quad writes a depth value that its own text quad, drawn microseconds later at that identical depth, then fails to beat ("less than", not "less-or-equal") - so the text silently doesn't draw. Which specific elements this hits depends on incidental depth-buffer contents from whatever 3D drawing happened to precede them that frame, which is exactly the "some buttons show their label, some don't, no obvious pattern" bug this fixes. A 2D overlay drawn last in painter's-algorithm order (later siblings on top - see _draw_element's docs) was never supposed to depend on the depth buffer at all.

on_destroy() #

Unregisters the draw callback registered in on_initialize.

on_initialize() #

Registers the draw callback and grabs a reference to the ui service's tree.