Speculative improvements to GuiState API

I am aware the feature isn’t released yet, but from what I’ve seen so far the API looks only partially useful to me because it has some blind spots. So I’m attempting to nip that in the bud by mentioning my concerns here.

Firstly I am worried about not being able to separate hover input from press input, as there is only a single GuiState property. If I want to have a GuiObject which is not clickable, but can detect hover, it looks like I will be forced to check for both GuiState.Idle and GuiState.Press. But I believe the latter will be problematic because it will continue to be set when the input’s position has been moved outside of the GuiObject, therefore giving an inaccurrate result if I only wanted to detect the hover. Probably the simplest solution API-wise is having another property that just sets whether the Press state is disabled or not.

Secondly, it’s not great that the API does not provide information about InputObjects. If you need to do anything more complicated than a simple button you are probably going to need to them. It would be nice to just get access to those for free, instead of having to manually rewrite the input processing code as we need to do currently. This would give us enough information to implement things like draggable objects, sliders, etc. Giving full information about InputObjects should be a standard for all input-related APIs IMO because they are so useful and are a requirement for doing anything non-trivial.

-- The current input objects which are causing
-- GuiState.Hover and GuiState.Press.
-- (With touch input these will be the same Touch, with
-- mouse input they will be MouseMovement and MouseButton1.)
GuiObject.PressInputObject
GuiObject.HoverInputObject

-- Fires whenever press/hover state changes.
-- Each signal provides both input objects because the press input may want to
-- know information that only the hover input has and vice versa.
-- i.e. hover input object has ownership of positional information,
-- press input object has ownership of activation information
GuiObject.PressChanged:Connect(function(IsPressing, PressInputObject, HoverInputObject) end)
GuiObject.HoverChanged:Connect(function(IsHovering, HoverInputObject, PressInputObject) end)
3 Likes

The main reason we’re looking to add GuiState is a convenience, as currently this requires a lot of manual code for hooking into InputBegan/InputEnded and checking various input types. So it isn’t really meant to encompass all usecases, as there are already other ways to track button state changes.

With the new GuiState property, these two snippets of code will be roughly equivalent, one is much shorter and easier to learn, as well as not hardcoding input types:

button:GetPropertyChangedSignal("GuiState"):Connect(function()
    if button.GuiState == Enum.GuiState.Press then
        applyPressedState(button)
    elseif button.GuiState == Enum.GuiState.Hover then
        applyHoverState(button)
    else
        applyIdleState(button)
    end
end
local isPressed = false
local isHovered = false

local function update()
    if isPressed then
        applyPressedState(button)
    elseif isHovered then
        applyHoverState(button)
    else
        applyIdleState(button)
    end
end

button.InputBegan:Connect(function(input)
    if input.UserInputType == Enum.UserInputType.MouseButton1 
        or input.UserInputType == Enum.UserInputType.Touch
        or input.UserInputType == Enum.UserInputType.ButtonA then
        isPressed = true
        update()
    elseif input.UserInputType == Enum.UserInputType.MouseMovement then
        isHovered = true
        update()
    end
end

button.InputEnded:Connect(function(input)
    if input.UserInputType == Enum.UserInputType.MouseButton1 
        or input.UserInputType == Enum.UserInputType.Touch
        or input.UserInputType == Enum.UserInputType.ButtonA then
        isPressed = false
        update()
    elseif input.UserInputType == Enum.UserInputType.MouseMovement then
        isHovered = false
        update()
    end
end

This works in the common case where you have 3 different states for your button, but isn’t enough for more advanced usecases.

I think that you have a good point though about coupling hover and press. The reason why GuiState is setup like this is because it’s actually an engine limitation - currently it’s not possible for something to be in the pressed state without also being hovered. The behavior where you move the mouse off of a button while holding left click currently results in leaving the pressed state. We shouldn’t be exposing limitations in the public API like that though.

I think your PressChanged/HoverChanged events are a good idea, as it would remove a lot of the hardcoded input type checking that’s currently required when you need the InputObject. It would pair well with the Activated event which is also input agnostic. There was also an internal request for something similar. Exposing the InputObject pointers as properties on GuiObject is something we probably won’t do though.

I’m talking with my team to see if we’ll make changes to this before enabling it.

All that said, during RDC we announced that styling was going to be added as a feature, and you may have noticed these objects appearing in the API dump (StyleSheet, StyleRule, StyleDerive, StyleLink). This feature is currently in development. With styling it will be possible to declaratively define hover and pressed states using :hover and :pressed selectors, similar to CSS. So for the simple case you won’t need manual event management with code.

8 Likes

Hard agree on this one. The smaller the button, and the more movement expected, the worse this problem gets. For example, I implemented a custom scrollbar recently (not because ScrollingFrame is deficient, just wanted extra style). It was nearly unusable using the normal methods of press detection, so I ended up only listening for InputBegan on the UI object, and resorting to mouse location polling via UserInputService:GetMouseLocation() until a matching UserInputService.InputEnded was fired.

I would rather not have Roblox bring forward this limitation in new APIs, because it makes it ever harder to fix this in retrospective.

2 Likes

Is GuiState supposed to reset to Idle when the user stops clicking on the button but is still hovering over it? This seems like a bug.

Yeah, unfortunately that’s “correct” behavior for now (but it shouldn’t be):