Skip to content

UIComponent: Optimize mouseMove and dragMouse methods #156

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

Merged
merged 7 commits into from
Mar 4, 2025

Conversation

Johni0702
Copy link
Contributor

@Johni0702 Johni0702 commented Feb 26, 2025

Previously these would both just traverse the entire tree and evaluate the constraints of all components.
In Essential, only components which can be dragged care about drag events, so most components don't, and since we primarily use the hoveredState from unstable Elementa, virtually no components need the mouseEnter/Leave listeners (which is what mouseMove is for).

This commit thusly optimizes the two methods to only traverse the narrow subtree which has components that might be interested. For simplicity, this is a best-effort optimization (e.g. it may still visit components which have had such listeners at some point but no longer do now); it should however be completely correct in that it will behave idential to how it behaved prior to this commit, just consume less CPU where easily possible.

Only exception are the lastDraggedMouseX/Y properties which are impossible to accurately emulate without traversing every component. They should really have been private and only in Window to begin with... This commit compromises by accepting that their behaivor may be different now if set manually or if dragMouse is called manually, but still preserves the rough meaning if it's merely read from in e.g. mouseRelease. The exact behavior of these properties was sufficiently unexpected that hopefully no one will have tried to rely on their exact behavior to begin with.

Note that the Flags class and tracking introduced in this commit also support Effects, despite neither of the two methods being available in Effects. This is because we'll also use the same mechanism with animationFrame in the future.

(technically depends on #155 to avoid merge conflicts (well, that didn't work) but is otherwise independent; the first three commits are from those PRs and have already been reviewed, this PR is the last commit only)

The old API is difficult to work with because it will continue to render
floating components even if they've been removed from the component
tree. Additionally it can CME if components are removed from the
floating list during rendering, further complicating the workarounds
required.

This new API fixes the issue by tracking when components are
removed/added from/to the tree and updating its internal floating list
accordingly.
It also allows setting the floating state at any time, even before the
component has a parent, another thing the old API did not support.

The order in which floating components appear also differs in the new
API. While the old API showed floating components in the order in which
they were set to be floating, this often isn't all too useful when the
order in which components are added/removed to/from the tree is not
particularily well defined. As such, the new API choses to instead order
floating components in exactly the same way as they appear in the
component tree (pre-order tree traversal, i.e. first parent, then
children). This results in consistent ordering and is generally the
order you want for nested floating components to behave in a useful way.

This has been implemented as a new, completely separate API instead of
an ElementaVersion primarily to easy migration (the new API can be used
even with Windows still on older ElementaVersions; both APIs can be used
at the same time) but also because there isn't anything reasonable the
old-API methods in `Window` could do in the new version, they really
should have been internal to begin with.
Intended to be used in almost all places where `animationFrame` was used
before (only major exception being animated constraints, which will
receive a different replacement).

Unlike `animationFrame`, which runs at a fixed rate of (by default) 244
times per second, the [UpdateFunc] API is variable rate, meaning it'll
be called exactly once per frame with the time that passed since the
last frame.
This allows it to match the true framerate exactly, animations won't
slow down under high load, and it won't waste tons of CPU time on
traversing the entire tree potentially mutliple times each frame.

The UpdateFunc API notably also allows modifying the component hierarchy
and accessing layout information directly from within a UpdateFunc call,
both of which are quite tricky to do correctly from `animationFrame`.
Previously these would both just traverse the entire tree and evaluate
the constraints of all components.
In Essential, only components which can be dragged care about drag
events, so most components don't, and since we primarily use the
`hoveredState` from unstable Elementa, virtually no components need the
mouseEnter/Leave listeners (which is what mouseMove is for).

This commit thusly optimizes the two methods to only traverse the narrow
subtree which has components that might be interested.
For simplicity, this is a best-effort optimization (e.g. it may still
visit components which have had such listeners at some point but no
longer do now); it should however be completely correct in that it will
behave idential to how it behaved prior to this commit, just consume
less CPU where easily possible.

Only exception are the `lastDraggedMouseX/Y` properties which are
impossible to accurately emulate without traversing every component.
They should really have been private and only in Window to begin with...
This commit compromises by accepting that their behaivor may be
different now if set manually or if dragMouse is called manually, but
still preserves the rough meaning if it's merely read from in e.g.
`mouseRelease`. The exact behavior of these properties was sufficiently
unexpected that hopefully no one will have tried to rely on their exact
behavior to begin with.

Note that the Flags class and tracking introduced in this commit also
support `Effect`s, despite neither of the two methods being available in
`Effect`s. This is because we'll also use the same mechanism with
`animationFrame` in the future.
Base automatically changed from feature/update-func-api to master February 27, 2025 10:43
Copy link
Contributor

@RedEpicness RedEpicness left a comment

Choose a reason for hiding this comment

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

Not deeply familiar with the systems involved, but looks good to me

put(Window::class.java, Flags(0u))
}
fun initialFor(cls: Class<*>): Flags = cache.getOrPut(cls) {
flagsBasedOnOverrides(cls) + initialFor(cls.superclass)
Copy link
Member

Choose a reason for hiding this comment

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

Wouldn't it make more sense to add the flags from overrides onto the initial flag set?

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's literally what this does? I don't understand.

Copy link
Member

Choose a reason for hiding this comment

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

I'm just being pedantic since if the left side is the first one and the right side is the other one, then it would imply that in this case, the override is the first one and the initial flags are the second.

@Johni0702 Johni0702 merged commit c199a84 into master Mar 4, 2025
1 check passed
@Johni0702 Johni0702 deleted the feature/optimize-mouse-move-drag-tree-traversal branch March 4, 2025 06:56
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

3 participants