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

UI input refactor #2474

Open
wants to merge 20 commits into
base: master
Choose a base branch
from

Conversation

MechWarrior99
Copy link
Contributor

PR Details

This refactor focuses on the input handling of the Runtime UI system, making it easier to understand, and more consistant.

  • Moved picking and input handling out of UIRenderFeature and in to UISystem, this means it now happens in the Update loop instead of the Draw loop.
  • Changed naming of Touch and Click to Pointer.
  • Added more properties to the PointerEventArgs (formerly TouchEventArgs), to give more control, and in the future allow for UIElements to be able to 'capture' a pointer's input (to allow for better drag actions).
  • Added a UIDocument that acts as a container for a UI's state and info required for it. Along with a EntityProcessor for UIComponents to create these and pass them to the UISystem.

Additional Notes

I am not sure about the naming of UIDocument, doesn't quite feel the most descriptive. And the term 'document' feels like it could be used better else where later on perhapse. Maybe in the editor to refer to assets which can be opened, or maybe some other file format. Nothing else was coming to me in the moment though.

Types of changes

  • Docs change / refactoring / dependency upgrade
  • Bug fix (non-breaking change which fixes an issue)
  • New feature (non-breaking change which adds functionality)
  • Breaking change (fix or feature that would cause existing functionality to change)

Checklist

  • My change requires a change to the documentation.
  • I have added tests to cover my changes.
  • All new and existing tests passed.
  • I have built and run the editor to try this change out.

@Doprez
Copy link
Contributor

Doprez commented Oct 3, 2024

Are there any things to look out for with the consolidation of mouse and touch inputs? Im personally happy to have one generic pointer event handler as I dont think the actual events between mouse and touch change much.

@MechWarrior99
Copy link
Contributor Author

Are there any things to look out for with the consolidation of mouse and touch inputs? Im personally happy to have one generic pointer event handler as I dont think the actual events between mouse and touch change much.

Actually it was already consolidated as pointer input, but was refered to as touch, and the touch routed event didn't contain info about the actual pointer. So really, it is just a name change and additional data.
The exception being inside of UpdatePointerOver in UISystem.PointerInput.cs, it uses the input.MousePosition still. In the future I want to change it to use the pointer position, but that felt a little larger scope than I wanted for this PR.

@Kryptos-FR
Copy link
Member

Be aware that renaming makes it a breaking change, so this branch cannot be merged until we reach the next major update of Stride.

@Kryptos-FR
Copy link
Member

Moved picking and input handling out of UIRenderFeature and in to UISystem, this means it now happens in the Update loop instead of the Draw loop.

What's the rationale? It is not explained in this PR. Is there an issue where this was discussed?

@MechWarrior99
Copy link
Contributor Author

Moved picking and input handling out of UIRenderFeature and in to UISystem, this means it now happens in the Update loop instead of the Draw loop.

What's the rationale? It is not explained in this PR. Is there an issue where this was discussed?

Ahh, sorry I forgot to go over that. No issue for this, but I'll open(s) for future work. There are a couple of reasons.

  1. A system made for handling rendering (the RenderFeature) was also handling user input, so was mixing responsibility.
  2. UISystem was handling the keyboard input already, so that meant that UI input was being handled in two seperate locations. Making it harder to find the code you were looking for and felt rather messy (to me).
  3. The pointer input was being handled in a Draw method instead of an Update method. I haven't dove deep enough in to the code to know if this truely matters. But having the pointer input be handled in the same location as the key input, and in an update loop instead of a draw loop felt like a better archetecture to me. This is a lesser of the 3 for me.

So this change means that all input is handled in the same class, i nthe same update method, along with any sort of picking related logic and utilities.

@MechWarrior99
Copy link
Contributor Author

Be aware that renaming makes it a breaking change, so this branch cannot be merged until we reach the next major update of Stride.

Yup, thanks for the clarification. I made sure to mark the checkbox noting the breaking changes. And it is fine, there are other breaking changes to the UI system I want to make later on anyway, so best to do them all at once anyway. I will open issues for it/them a bit later to get a discussion on them going, once I have a more fleshed out plan.

@Kryptos-FR
Copy link
Member

Kryptos-FR commented Oct 3, 2024

Ok so no particular reason to move the code just a feeling. Well there is good reason to do the picking in the rendering loop, that's because it involves the graphics api to do so.

Unless there is a good reason to change that behavior, I'd prefer we don't touch it. As the saying goes, if I ain't broke don't fix it.

Update loop and draw loop might run at different rate, so picking during the update loop could be incorrect and involve issues with multi threading.

@xen2 what's your take on this?

To me this PR is a good example of why it is better to discuss it with the team (either by opening an issue or on our Discord server) before starting to do any work on your own.

Final note: don't get me wrong, I'm all about refactoring if that makes the code cleaner. But we need to make sure it doesn't introduce any regression and is correct in term of architecture.

@Eideren
Copy link
Collaborator

Eideren commented Oct 3, 2024

@Kryptos-FR @MechWarrior99 did discuss this PR with us, see https://discord.com/channels/500285081265635328/500290856532574209/1283586888409550868
I don't think we should dissuade our users from contributing towards refactoring some of our systems if those make sense. I haven't had time yet to go through the changes this PR introduces.

I did mention over there that picking inside of Draw would be more correct though, but let's check the changes before dismissing this wholesale.

@MechWarrior99
Copy link
Contributor Author

Hmm, I wouldn't say there isn't a good reason to move it @Kryptos-FR. Mind you I am not sure if the draw and update run at different rates (think they do?). But if the draw loop does run faster than the update loop (where the input events are populated) then it could raise the same event multiple times, which would be a issue for pointer pressed and released events. And if the draw does run slower, it could completely miss inputs, as the input events are cleared at the start of each update by the InputManager. This also means that all pointer events are raised on their UIElements in the draw loop, so any code that responds to those events will also run in the draw loop.

I probably should have said this more explcitly in the PR from the start!

As for graphics usage, while it does use the graphics API, in the end it actually is only the viewport (used to unproject a screen point to world space using a matrix, to get a ray). And the size of the dispaly (used for getting the world view projection matrix of a UIComonent's UI). To me these feel pretty minor compared to the amount of input related code, and for the most part do not require the current render state, so don't need to be in the draw loop.

(Full discloure, I am not completely sure that the way I am getting the viewport is the best in this PR, but from looking around the code base it seemed to be the most correct way to do.)

Looks like Eideren beat me to it. But yeah I did actually ask on discord before starting work, perhapse I should have linked it before, that's my bad. Maybe it is better to make a Issue for these things so they are a little easier to notice and not get hidden?

I do totally get not wanting to touch code that is already working fine, as it can introduce bugs and regressions as you say. But I think not refactoring old code to be easier to work with hampers the code quality and accessability of the project. If after review if its decided that this isn't the way to go about to, so be it.

Copy link
Collaborator

@Eideren Eideren left a comment

Choose a reason for hiding this comment

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

Thanks for the contribution, I do like the naming scheme changes, matching how with the Input API makes it easier to reason about.

sources/engine/Stride.UI/PointerEventArgs.cs Outdated Show resolved Hide resolved
@@ -847,25 +847,19 @@ protected override void OnPreviewTouchMove(TouchEventArgs args)
args.Handled = true;
}

private static void RaiseLeaveTouchEventToHierarchyChildren(UIElement parent, TouchEventArgs args)
private static void RaiseLeavePointerEventToHierarchyChildren(UIElement parent, PointerEventArgs args)
Copy link
Collaborator

Choose a reason for hiding this comment

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

Maybe RaisePointerLeaveEventInHierarchy to match similar calls' naming scheme.

Comment on lines +60 to +74
public PointerEventArgs Clone()
{
return new PointerEventArgs()
{
Device = Device,
PointerId = PointerId,
Position = Position,
DeltaPosition = DeltaPosition,
DeltaTime = DeltaTime,
EventType = EventType,
IsDown = IsDown,
WorldPosition = WorldPosition,
WorldDeltaPosition = WorldDeltaPosition
};
}
Copy link
Collaborator

Choose a reason for hiding this comment

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

Would it make sense to make this class a record ? Cloning would be automatically generated and would enable a couple of pattern matching operations

Copy link
Contributor Author

Choose a reason for hiding this comment

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

It inherits from RoutedEventArgs which inherits from System.EventArgs, so that would need to change. I haven't read up on all the details of record yet so can't say for certain if it makes sense and what the knock-on side effects would be. But from what I remember, I think it could, if we are fine with not using System.EventArgs

sources/engine/Stride.UI/UISystem.PointerInput.cs Outdated Show resolved Hide resolved
{
if (renderUIElement.Page?.RootElement == null)
if (renderContext == null || sceneSystem == null || sceneSystem.GraphicsCompositor == null)
Copy link
Collaborator

Choose a reason for hiding this comment

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

Shouldn't those checks be the callers' responsibility, or passed by the caller, I feel like the method should do what its name implies, if the caller calls it in an invalid state then it should error out instead of silently failing.

Not too keen on the rest of the method either, the editor workaround is too fragile, and multi camera setup won't work appropriately because of the shared renderContext and Viewport0. You have to move logic relying on those to the render loops

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Shouldn't those checks be the callers' responsibility, or passed by the caller

Not sure, it is a private method involving the values of the class that is calling it. And if you called it else where you would want the same checks, no? I was also copying the style of UpdateKeyEvents() in UISystem which has the checks in itself as well.

the editor workaround is too fragile, and multi camera setup won't work appropriately because of the shared renderContext and Viewport0.

I actually agree with both of these. However, to me this feels like an issue of either me not knowing the correct way to get the required info, or the engine not exposing the info in a way that can be used. This is especially the case for Viewport, because as I was looking through the code to try to understand how it works and is used, there are lots of places the mention/refer to supporting multiple viewports, but not actually support it. Not sure if using Viewport is even needed, but I haven't been able to figure out exactly how and why Viewport does.
See my reply to UIRenderFeature for my thinking.

Copy link
Collaborator

Choose a reason for hiding this comment

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

Most of that would come from inheriting from RenderFeature and overriding RenderFeature.Draw, which is called by the forward renderer, scene renderer and such.
The data you require there is setup by those systems right before calling draw. You can get your viewport from context.CommandList.Viewport for example.
You might be able to write a system to preemptively gather active viewports before draw, retrieve the scene system and such to take care of all of that in Update but it'll very likely look like an even larger hack than it already is

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Instead of preemptively gathering them, would probably be simple to cache a list of viewports from the last draw. Either just for the UI, or could make it more generic for the rendering system so in the future other systems could use them. I do feel like there is possibility of other gameplay/interactive use for the viewports.

Copy link
Collaborator

Choose a reason for hiding this comment

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

That's kind of a hack, not sure what kind of side effects this would create given that those would always be one frame behind Update()

Copy link
Contributor Author

Choose a reason for hiding this comment

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

I can see how it could be a hack. I think of it more as using the most recently fully rendered data. The only rendering related data that is used is the GraphicsDevice.Presenter.BackBuffer's size, and the Viewport. Unless the viewports change size or location there will be no idfference as they are otherwise constant. If there is a change, the only possible side effect would be the pointer position being incorrect until the next draw.

The ForceAspectRationSceneRenderer.cs is the only place I can see that updates a Viewport in a way that would impact it.

The chance of the a pointer position being off by normally a small amount for a frame seems pretty tiny to me. And worth the benefits to me.

(Of course there could be other side effects I don't see. But the code is pretty straight forward seeming to me as to what is used when. But I could be missing something!)

Comment on lines +219 to 230
var uiPointerEvent = new PointerEventArgs()
{
Action = TouchAction.Down,
Timestamp = time,
ScreenPosition = currentTouchPosition,
ScreenTranslation = pointerEvent.DeltaPosition,
Device = pointerEvent.Device,
PointerId = pointerEvent.PointerId,
Position = pointerEvent.Position,
DeltaPosition = pointerEvent.DeltaPosition,
DeltaTime = pointerEvent.DeltaTime,
EventType = pointerEvent.EventType,
IsDown = pointerEvent.IsDown,
WorldPosition = intersectionPoint,
WorldTranslation = intersectionPoint - state.LastIntersectionPoint
WorldDeltaPosition = intersectionPoint - uiDocument.LastIntersectionPoint
};
Copy link
Collaborator

Choose a reason for hiding this comment

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

record's clone with feature might work well to clarify this logic here

{
if (input == null || !input.HasMouse)
if (input == null || !input.HasMouse) // no input for thumbnails
Copy link
Collaborator

Choose a reason for hiding this comment

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

Same deal, shouldn't this be the caller's responsibility ? Or maybe create a Try proxy for this call

Comment on lines +129 to +133

if (renderContext == null)
renderContext = RenderContext.GetShared(Services);

UpdatePointerInput();
Copy link
Collaborator

Choose a reason for hiding this comment

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

See my other comment in UpdatePointerInput

Its functionality is already covered by IsPointerDown defined in UIElement. Tested at runtime to ensure there is no discrepancy in the behavior when using IsPointerDown.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Projects
None yet
Development

Successfully merging this pull request may close these issues.

4 participants