Utilities and internals

The helpers Ember builds itself out of — Util, Panel, Card, and the Metrics, Motion and Fx tables — for custom controls and your own GUI.

Ember hangs the parts it is made of off the module, so a control you register looks like one that shipped with the library rather than something bolted to the side of it.

Luau
local Util = Ember.Util

Everything here is stable API. It is also lower-level than the rest of the docs: reach for it when you are registering a control or painting your own GUI, not when a control already does the job.

Building instances#

Util.create is the constructor the whole library uses. Props, then children, with Parent applied last so a frame is never briefly parented while half configured.

Luau
local frame = Util.create("Frame", {
    Parent = slot,
    BackgroundColor3 = Ember.Theme.panel2,
    Size = UDim2.fromOffset(120, 26),
}, { Util.corner(6), Util.stroke(Ember.Theme.border) })
FunctionReturnsWhat it does
Util.create(class, props, children?)InstanceBuilds one. children is an array parented into it.
Util.label(text, size?, colour?, font?, richText?)TextLabelTransparent, left-aligned, vertically centred. Defaults to size 14 in the text role.
Util.icon(source, size?, colour?)ImageLabel | nilA glyph from an icon name, asset id or sprite table. nil if the source is unknown, having warned.
Util.setIcon(label, source)booleanRepoints an existing ImageLabel at another icon. false if the label or the source is wrong.

Util.label coerces a non-string, so a number is safe. richText is off unless you pass true.

Layout and shape#

Each returns a bare UI object for you to parent, or to pass in create's children array.

FunctionReturnsWhat it does
Util.corner(radius?)UICornerDefaults to Metrics.radius.
Util.pill()UICornerA 999-radius corner, for anything fully rounded.
Util.stroke(colour?, thickness?, transparency?)UIStrokeBorder-mode, round joins. Defaults to the border role at 1px.
Util.padding(all, left?, right?, top?, bottom?)UIPaddingEach side falls back to all, so padding(8) is even and padding(0, 9, 9) pads only the sides.
Util.list(gap?, direction?, extra?)UIListLayoutVertical, LayoutOrder-sorted. extra is merged in as props.
Util.hlist(gap?)UIListLayoutHorizontal, vertically centred.

Colour and style#

Luau
local look = Util.style("filled", Ember.Theme.accent)
 
local button = Util.create("TextButton", {
    BackgroundColor3 = look.fill,
    BackgroundTransparency = look.fillFade,
    TextColor3 = look.ink,
}, { Util.stroke(look.edge, nil, look.edgeFade) })

Util.style(name, tone) resolves one of four named looks against a tone colour and returns { fill, fillFade, ink, edge, edgeFade, name }. An unknown name falls back to outline, and name tells you which you actually got.

StyleLooks like
outlineBackground fill, toned text, a border. The default.
filledSolid tone with accentInk text.
ghostNo fill and no border until something else draws one.
softA faint wash of the tone, with a matching faded edge.

Util.brighten(colour, amount) lifts a Color3 toward white in HSV, amount being 01. Use it for a hover tint that keeps the hue.

Animation#

Util.tween(inst, props, info?) is TweenService:Create plus the two things every call site needed anyway: it cancels any in-flight tween touching the same properties, and it honours the effects config. When animation is off, or RespectReducedMotion is on and the player set that flag, it assigns the values immediately and returns nil rather than animating.

Luau
Util.tween(frame, { BackgroundColor3 = Ember.Theme.accent }, Ember.Motion.fast)

info defaults to Motion.fast. The return is the Tween, or nil when it applied instantly — so do not assume there is something to :Play() again.

Hover, pointer and input#

FunctionReturnsWhat it does
Util.hover(inst, enter, leave, region?)subscriptionRuns enter/leave on pointer or gamepad selection. region(inst) returning position, size narrows the hit area.
Util.surface(button, tone, glyph?, glyphRest?, hoverFill?)UIStrokeWires the standard hover: fill, edge tint and glyph tint. tone takes a theme role name or a Color3.
Util.interact(button, spec)tableSwaps icon, caption and rotation on hover and click. See below.
Util.drag(handle, callbacks)subscriptionPointer and touch dragging. onStart(input) returning false refuses the drag; onMove(position); onEnd().
Util.isDisabled(node)booleanWhether the node sits inside a disabled control, stopping at the window.
Util.rotatable(glyph)ImageLabelWraps a glyph in a frame so it can spin inside a list layout without shifting its neighbours.

Util.hover covers selection as well as the mouse, which is why controls respond to a gamepad. Both callbacks are wrapped, so one that errors warns rather than leaving the control stuck lit.

Util.interact's spec takes Glyph, Caption, and a Hover and Click table of { Icon, Text, Scale, Rotate, Hold }. It returns a table with SetIcon(name) and SetHoverIcon(name) for changing what the control rests at later — which a control whose meaning flips, like a minimise button, needs.

Drop what you take

hover, drag and watchPointer all return something with :Disconnect(). Hand it to win:Track(...) or disconnect it in Destroy, or the connection outlives the control.

The pointer helpers — pointerPosition, pointerInside, watchPointer, setCursor and Util.Cursors — are documented under Pointer and cursor helpers.

Cards and panels#

A control's row is a card. Controls.mount is the usual way in, since it also registers the row for search:

Luau
Ember.RegisterControl("Badge", function(section, opts)
    local card, slot = Ember.Controls.mount(section, opts)
    -- put your control in `slot`
    return { Instance = card }
end)

Ember.Card.new(parent, opts) is the same card without the registration, for a row outside a section. It returns six values:

ValueWhat it is
cardThe row frame
slotWhere your control goes, right-aligned
titleThe label TextLabel
lineThe horizontal row holding icon, labels and slot
iconThe glyph, or nil
fit(element, reserve?, minWidth?)Caps that element's width against the card, re-measuring as the window resizes

Ember.Panel.round(frame, radius?, sides?, colour?) rounds a frame and squares off named sides, for a panel that has to butt up against another.

Luau
Ember.Panel.round(myFrame, 8, { "top", "left" })

sides takes any of "top", "bottom", "left", "right". It returns false if the frame is not a GuiObject.

The measurement tables#

Read these so a control you build lines up with the ones that ship.

Luau
Ember.Metrics.control      --> 26, the height of every control
Ember.Metrics.ctlRadius    --> 6, their corner radius
Ember.Metrics.controlCol   --> 104, the minimum width of the right-hand column
TableHolds
Ember.Metricspad, gap, row, radius, winRadius, rail, topbar, statusbar, grip, control, ctlRadius, labelMin, controlCol
Ember.Motionfast (0.14s), base (0.22s), open (0.34s), shut (0.24s) — TweenInfos
Ember.FxThe dissolve's own numbers: tile, particles, burn, ember, emberHot, and the four timings

Metrics is read, not written

These are the values the library itself uses, so changing one changes controls already on screen in ways nothing re-measures. Read them; set sizes through the options each control offers.

Tune the dissolve through Configure rather than by writing to Fx.

Odds and ends#

CallReturnsWhat it does
Ember.VersionstringThe library version, "3.0.0"
Ember.ControlstableEvery control builder by name. Ember.Controls.mount is the one you call.
Ember.Util.options(first, second, third, key?)tableThe argument shuffling every control does, so ("Text", fn) and ({ Text =, Callback = fn }) both work in a control you wrote.
Util.calibratePointer()falseKept so older scripts still load. Calibration is no longer needed and it does nothing.

Section:ClosePopups(except?) shuts every open dropdown and picker in a section, optionally sparing one instance. A control that opens its own popup should call it so two menus are never open at once.