Luix v1.4.5 - UDim2 & Completion Fixes
UDim2.fromScale ↔ UDim2.fromOffset conversion that resolves through the parent chain
The existing UDim2 quick-fix can only flip forms when the value is
lossless — UDim2.new(0.5, 0, 0.3, 0) ↔ UDim2.fromScale(0.5, 0.3),
etc. It can't help when you want to materialise
UDim2.fromScale(1, 0.15) as concrete pixels because that requires
knowing the parent's size.
New refactor action: place the cursor on any
UDim2.fromScale(…) / UDim2.fromOffset(…) literal that's the value
of an element's Size = prop, hit Ctrl+. (or click the lightbulb),
and pick "Convert to UDim2.fromOffset(…)" (or fromScale). Luix
walks up the source-order parent element chain, multiplies through
each ancestor's scale until it hits a concrete pixel Size, and
emits the resolved value.
e("Frame", { Size = UDim2.fromOffset(800, 600) }, {
e("Frame", { Size = UDim2.fromScale(0.5, 0.5) }, {
e("Frame", { Size = UDim2.fromScale(1, 0.15) }), -- cursor here
}),
})→ Convert to UDim2.fromOffset(400, 45). The action's title shows
the computed value so you can confirm before accepting.
Recognises reactive :map(...) Sizes when walking the parent
chain — `Size = popupScale:map(function(s) return UDim2.fromOffset(460
- s, 360 _ s) end)
is treated as a 460×360 anchor, not as "non-literal, give up", because the coefficient comes straight out of the innerUDim2.fromOffsetcall. Accepts the three strict shapes for each axis:,_, and_
(so460 _ sands _ 460both work). Anything more elaborate (460 + bonus,clamp(s, 0, 1) _ 460`) skips that
ancestor rather than guessing — same "no fallback" principle as
below.
No invented numbers. When the parent chain can't be resolved
(no parent in source, every ancestor uses fromScale with no
fromOffset / :map(fromOffset(...)) anchor, a mixed-axis
UDim2.new(0.5, 10, …) somewhere in the chain, or a non-literal
Size = props.size), the action stays hidden rather than emitting
a plausible-looking-but-wrong fallback value. A missing lightbulb is
better than silently shifting your layout.
"Calculate Size from children" — the second UDim2 action
Companion action that fires on a parent element whose Size is
UDim2.fromScale(…) (or fromOffset(…)) and computes the implied
pixel size from its children. Handles:
- Children with literal
UDim2.fromOffset(W, H)Sizes — pooled
according to the layout rule (see below). UIListLayout—FillDirection(Vertical / Horizontal) and
thePadding = UDim.new(0, N)gap. Children are summed along the
fill axis, max-pooled across.UIPadding—Padding{Top,Bottom,Left,Right} = UDim.new(0, N)margins added to the pooled total.- Layout decorators (
UICorner,UIStroke,UIGradient,
UIFlexItem,UIScale, theUI*Constraintfamily) are
recognised and skipped — they don't take up content space.
Without a UIListLayout, falls back to the bounding-box rule
(max(child.size) on both axes), suitable for free-positioned
children. The action stays hidden when any contentful child has a
Size we can't reduce to pixels (mixed scale, a variable, a :map(...)
binding without a clean coefficient) — same honest-or-quiet principle
as the chain-walking action.
Component completions now auto-import the require
Accepting a workspace-component suggestion (DailyQuestCard,
StylizedButton, …) from Luix's completion dropdown used to insert
just the identifier, leaving the file broken until the user manually
added local DailyQuestCard = require(…) at the top. The completion
item now carries an additionalTextEdits entry that inserts the
require line — at the same position the existing
luix.autoImport quick-fix would — whenever:
- The component lives in a different file than the current one
(same-file definitions need no import). - A
local <Name> = require(…)for it isn't already present.
The import path respects luix.autoImport.style (relative /
alias) and the luix.autoImport.aliases mapping, identical to the
existing diagnostic-driven quick-fix. The item's detail line shows
the resolved path so you can see what's about to be inserted before
accepting.
Workspace components only surface in files that look like UI code
Typing a single letter in a server script, a pure-logic module, or
anywhere else with no UI in sight used to fuzzy-match every workspace
function starting with that letter (Pro → PopularGamepasses,
ProductRegistry.GetGamepassProduct, …) as if they were Luix
components. Two compounding bugs:
looksLikeUIFilewas too permissive — the "file has at least
one function" check fired on any file, since the parser indexes
every function definition (not just component-shaped ones). A
server-sideProductRegistrymodule with utility functions counts
as "has functions" → falsely flagged as UI. Now relies on the
precise signals only: an actual factory call (e(...),
New "...",create "...",Roact.createElement(...)) or a
require()of a known UI framework. Files without either —
server scripts, DataStore utilities, plain libraries — won't
surface workspace components.knownComponentNamesdidn't filter bylooksLikeComponent—
the workspace index stored every function definition under its
last-segment name, and the completion provider treated all of
them as components. Now filtered to functions that either return
an element call (detectedBaseset) or carry an explicit
@extends ClassNameannotation, matching the rule the sidebar
already uses.
Suppress workspace components inside Luau type X = … declarations
Typing export type Gamepass| to write a type alias used to dump
every workspace component starting with Gamepass into the dropdown
(GamepassCard, GamepassHero, GamepassInfoOverlay, …) because
the direct-call detector treated a bare identifier with no preceding
keyword as a value-position function call. Both the type-name slot
(export type X|) and the RHS (type X = MyComp|) are now
detected as type-position and the workspace-component suggestions
are suppressed. Covers type, export type, and the (uncommon)
local type prefixes.
Rect editor — X / Y now accept negative values
Roblox treats negative ImageRectOffset as panning the visible rect
to the right / down relative to the source texture origin — a
legitimate use case, especially when scroll-zooming out (below 100%)
to position a rect that extends past the top-left of the source. The
editor used to hard-clamp X and Y to [0, maxRect()], so dragging
or arrow-stepping past the left / top edge silently snapped back to
zero. Both axes now use the symmetric range [-maxRect(), maxRect()]
across all four input paths: numeric field, wheel step, arrow step,
and drag + resize handles. W and H stay non-negative (Roblox doesn't
accept negative ImageRectSize).