Skip to content

element

FreeBodyEngine.ui.element #

ElementStates #

Bases: Enum

The interaction states a UIElement can be in, matching the state-style override keys documented above (note FOCUSED's value is "selected", not "focused").

CLICKED = 'clicked' class-attribute instance-attribute #

FOCUSED = 'selected' class-attribute instance-attribute #

HOVER = 'hover' class-attribute instance-attribute #

NORMAL = 'normal' class-attribute instance-attribute #

GenericElement(tag=None) #

Base for anything that can parent UIElements - shared by RootElement and UIElement itself so elements nest arbitrarily deep.

Args: tag: Optional identifier for this element, not used by the layout/draw code itself.

children = {} instance-attribute #

styles = {} instance-attribute #

tag = tag instance-attribute #

add(element) #

Adds element as a child of this element.

remove(element) #

Removes element from this element's children.

Layout(x, y, width, height) dataclass #

A computed screen-space rectangle: top-left (x, y) plus width/height, in pixels.

height instance-attribute #

width instance-attribute #

x instance-attribute #

y instance-attribute #

RootElement(width, height, styles={}) #

Bases: GenericElement

Root UI element.

Supported styles:

padding
padding_left
padding_right
padding_top
padding_bottom

layout:
    "vertical"
    "horizontal"

gap

Args: width: Width of the root layout area (typically the window/framebuffer width), in pixels. height: Height of the root layout area (typically the window/framebuffer height), in pixels. styles: Root-level styles - see the style documentation above.

height property writable #

See width - the same live-vs-stale fix, for height.

layout = Layout(0, 0, width, height) instance-attribute #

styles = styles instance-attribute #

width property writable #

Current root width, in pixels. A property reading straight from self.layout (the one place UIManager.resize() actually updates) rather than a separate stored value - width/height used to be their own plain attributes, set once at construction and never touched again, while resize() only ever updated self.layout's copy. Every window resize after the first frame left this permanently stuck at whatever size the window happened to be at launch: any code reading root.width/root.height directly (a content area sizing itself to "the window height minus my header/footer," say) silently kept computing against a stale snapshot forever after, however many times the real window resized - exactly the shape of "layout looks right on launch, then the proportions are wrong forever after," which a tiling window manager (retiling on every window open/close) triggers constantly. A property means every existing read of root.width/root.height just starts seeing the live value with no call-site changes.

calculate_layout() #

Calculate the root content area and recursively calculate the layout of all children.

set_styles(styles) #

Replaces the root's styles wholesale.

UIAnimation(element, style, end_value, duration, curve=Linear()) #

An in-progress animation of one style on one UIElement, created by UIElement.set_style(..., duration=...) and driven forward by UIElement._update() each frame until it reaches duration and removes itself.

Parameters:

Name Type Description Default
element UIElement

The element whose style is being animated.

required
style str

Name of the style to animate.

required
end_value any

The value style should reach once the animation finishes.

required
duration float

How long the animation takes, in seconds.

required
curve Curve

Easing curve applied to the 0-1 progress before interpolating.

Linear()

Raises:

Type Description
ValueError

If style's current value isn't an animatable type, or if it isn't compatible with end_value (see _validate_animatable).

curve = curve instance-attribute #

duration = duration instance-attribute #

elapsed = 0.0 instance-attribute #

element = element instance-attribute #

end_shape = self._get_shape(self.end_value) instance-attribute #

end_value = end_value instance-attribute #

finished = False instance-attribute #

start_shape = self._get_shape(self.start_value) instance-attribute #

start_value = self._capture_start_value() instance-attribute #

style = style instance-attribute #

remove() #

Detaches this animation from its element, stopping it from being updated further.

update() #

Advances the animation by one frame's delta time, writing the interpolated value straight into the element's style, and removes itself from the element once duration has elapsed.

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
)