Home › Utilities
Interact
Interactive UI keeps transient state the rest of the tree must agree on: only one dropdown open, one control hovered, one dialog modal. Writing that coordination by hand means every menu knows how to close every other menu. Flux's interact helpers replace the bookkeeping with an exclusive group (a container guaranteeing at most one active boolean node) plus two pointer predicates, pointIn and sunk, for hit-testing against real GuiObjects. (They are also available under the Flux.Interact namespace.)
Exclusive groups
Flux.exclusive() creates a group. Members are ordinary boolean nodes (a menu's Visible state, a control's hovering state), and the group guarantees activating one deactivates whichever was active before:
local floating = Flux.exclusive() -- one group per concern, made once
floating:toggle(dropdown.Visible) -- flip on click; opening closes any open menu
floating:activate(contextMenu.Visible) -- open explicitly (closes the dropdown)
floating:deactivate(contextMenu.Visible) -- close explicitly
floating:clear() -- close the active one, whichever it isThe semantics, precisely:
activate(state)setsstatetotrueand sets the previously active member tofalse(if different). Both writes land before the next flush, so effects only ever observe the settled result.deactivate(state)setsstatetofalse, releasing the active slot whenstateholds it. Deactivating a member that is not active still sets itfalse, so handlers can call it unconditionally.toggle(state)activates when inactive, deactivates when active. The read is untracked, so toggling inside an effect never subscribes it.clear()deactivates the active member, if any: the outside-click dismiss that doesn't need to know which menu is open.
Groups hold plain state, not subscriptions: members need no cleanup, and a node can freely belong to several groups.
TIP
One group per concern, not per menu. A floating group shared by every dropdown, context menu, and select gives the "opening one closes the rest" behavior across your whole UI for free.
pointIn
Whether a point lies within a GuiObject's absolute bounds (inclusive). Coordinates are inset-relative, so InputObject.Position and AbsolutePosition line up:
-- Dismiss an open menu when a press lands outside it and its anchor:
if
not Flux.pointIn(input.Position.X, input.Position.Y, menu)
and not Flux.pointIn(input.Position.X, input.Position.Y, anchor)
then
floating:clear()
endpointIn reads only AbsolutePosition/AbsoluteSize: it ignores rotation and does not consider occlusion. For "is something else on top", use sunk.
sunk
Whether input at a point would be claimed by an Active gui (or GuiButton) rendered above object. Hover states built from InputChanged alone stay lit when a floating menu covers the control; sunk is the missing test:
local hover = Flux.exclusive()
local hovering = Flux(false)
-- in the control's InputChanged handler (MouseMovement / Touch):
if Flux.sunk(input.Position.X, input.Position.Y, control) then
hover:deactivate(hovering)
else
hover:activate(hovering)
end
-- in InputEnded:
hover:deactivate(hovering)NOTE
sunk walks GetGuiObjectsAtPosition on the object's ancestor BasePlayerGui. Outside one (a plugin dock, a SurfaceGui in workspace) nothing can claim the point, so it yields false. pointIn and exclusive groups run anywhere, including headless Luau; sunk needs the engine.