Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Fix wrong focus neighbor for grid-aligned 0 separation controls #95500

Merged

Conversation

tetrapod00
Copy link
Contributor

@tetrapod00 tetrapod00 commented Aug 13, 2024

Fixes #42965.

In these videos, I am inputting "ui_right" by pressing the right arrow key.
Current behavior:

Godot_v4.3-rc1_mono_win64_4MnReWYwAc.mp4

This fix:

godot.windows.editor.x86_64_AlkCfDeYt8.mp4

The current algorithm for finding focus neighbors seems to check each side of a control against each side of a potential closest neighbor, and choose the control which is closest. In cases where there is non-zero separation between controls, this works just fine. But in cases where there is zero separation between controls, and the controls are arranged in a grid, multiple potential controls are equally close (e.g, up-left, up. and left will all have a distance of 0). This introduces a bias towards the first found neighbor control (empirically, controls that are to the left and/or up from the current control).

This fix adds a tie-breaking step in the case where multiple controls are equally close. It sorts by the minimum distance between the centers of the current control and prospective closest neighbors, which works nicely for grid-aligned controls.

Since it only runs in the tied case, this shouldn't affect existing behavior in cases where there is any separation between controls. The default theme adds separation, and anyone using zero-separation grid containers is very unlikely to be dependent on incorrect existing behavior.

My C++ will need extensive changes for style and efficiency - it's been a while since I wrote any!

Edit: Okay, that's my best attempt at C++ style. I'm aware that my code is different in style than the surrounding function, but I wanted to leave in verbose names and comments in case the algorithm itself needs to be changed. I also don't know what I don't know in terms of helper functions to use.

@tetrapod00 tetrapod00 requested a review from a team as a code owner August 13, 2024 20:40
@tetrapod00 tetrapod00 marked this pull request as draft August 13, 2024 20:44
@tetrapod00 tetrapod00 force-pushed the zero-separation-focus-neighbor branch from 9d6a8eb to 41918a7 Compare August 13, 2024 20:59
@tetrapod00 tetrapod00 marked this pull request as ready for review August 13, 2024 21:00
@YeldhamDev YeldhamDev added this to the 4.4 milestone Aug 13, 2024
@tetrapod00
Copy link
Contributor Author

tetrapod00 commented Aug 13, 2024

Ah, there's another case, where tie-breaking by distance is incorrect. Video of my PR:

godot.windows.editor.x86_64_Kv4dC3rqTG.mp4

@tetrapod00
Copy link
Contributor Author

tetrapod00 commented Aug 13, 2024

This video shows tie-breaking the prospective neighbors by the which one is most aligned with the desired input direction. This works in the basic grid-aligned case, and it works well enough for the center button in the edge case presented. However, it does not work intuitively when moving from the large buttons to the small ones.

godot.windows.editor.x86_64_WioxLWvdyS.mp4

Satisfying all the intuitive behaviors of neighbor focus would require rethinking the entire selection algorithm, rather than just patching a tie-breaking step at the end.

I propose solving the narrow problem of 0 separation grid-aligned controls in this PR, using tie-breaking by the closest aligned control. This solves the obvious pain-point of a 0 separation GridContainer, as well as some other grid-aligned cases. In other cases, users can add manual neighbor overrides (as they are probably already doing). Existing behavior in non-zero separation cases will remain unchanged.

Updated MRP with the edge case:
mrp-gridcontainer-focus.zip

@tetrapod00 tetrapod00 force-pushed the zero-separation-focus-neighbor branch from 41918a7 to 9c66f49 Compare August 13, 2024 23:00
@tetrapod00 tetrapod00 requested a review from Calinou August 15, 2024 02:29
@tetrapod00 tetrapod00 force-pushed the zero-separation-focus-neighbor branch from 09cf070 to ade7c1e Compare August 15, 2024 06:24
@KoBeWi
Copy link
Member

KoBeWi commented Aug 30, 2024

In your new test project it's not possible to select corner buttons:

godot.windows.editor.dev.x86_64_dqu2hROmQK.mp4

You can select them using Tab, but the order is odd.

With old behavior it was at least partially possible:

godot_gCjmVNLvrB.mp4

That's a corner case though, so probably fine.

Copy link
Member

@KoBeWi KoBeWi left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Overall it's better functionality-wise. Can't say much about the code, as it's rather complex .-.

Copy link
Member

@AThousandShips AThousandShips left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM

@tetrapod00
Copy link
Contributor Author

In your new test project it's not possible to select corner buttons:

My goal with this PR was to make the common case behave correctly, and make it the user's responsibility to use manual neighbors to fix literal corner cases like this. As it is, the common case is already wrong and the user's responsibility to fix with manual neighbors, so this is an improvement

If it's a hard requirement that every control on the screen must be accessible through some permutation of left, right, up, down inputs - that might be possible, but I'm not even sure if the existing system can guarantee that.

I do think that the existing algorithm + this patch is not exactly what would be designed from first principles for an intuitive default neighbor selection algorithm - that would use some weighted combination of closeness and alignment with direction. But I wanted to make a change that does not affect the existing behavior except when tie-breaking.

@tetrapod00 tetrapod00 force-pushed the zero-separation-focus-neighbor branch from ade7c1e to 8985a77 Compare August 30, 2024 18:19
@tetrapod00 tetrapod00 force-pushed the zero-separation-focus-neighbor branch from 8985a77 to c5ef2e2 Compare September 7, 2024 01:12
@tetrapod00 tetrapod00 requested a review from akien-mga September 8, 2024 03:34
@akien-mga akien-mga merged commit 5216ede into godotengine:master Sep 8, 2024
20 checks passed
@akien-mga
Copy link
Member

Thanks!

@tetrapod00 tetrapod00 deleted the zero-separation-focus-neighbor branch September 8, 2024 21:32
if (d < r_closest_dist) {
r_closest_dist = d;
*r_closest = c;
} else if (should_tiebreak && d == r_closest_dist) {
// Tie-break in favor of the control most aligned with p_dir.
if (p_dir.dot((center - p_center).normalized()) > p_dir.dot((closest_center - p_center).normalized())) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Now we are using the offset angle, this is why the corner buttons cannot be selected.

Wouldn't it be better to compare the absolute value of the cross product here?

This makes it possible to select the corner buttons.

001

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

That might be better, indeed. When I wrote this PR I only considered distance and dot product as options for tiebreaking.

It would probably be good to write some unit tests for focus neighbor behavior, so we can make changes like this safely.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
7 participants