Skip to content

Introduce PresentationSource, move some responsibilities from TopLevel#20624

Open
kekekeks wants to merge 24 commits intomasterfrom
feature/presentation-source
Open

Introduce PresentationSource, move some responsibilities from TopLevel#20624
kekekeks wants to merge 24 commits intomasterfrom
feature/presentation-source

Conversation

@kekekeks
Copy link
Member

@kekekeks kekekeks commented Feb 6, 2026

See #20622

This PR mostly moves stuff around, no observable behavioral changes yet, TopLevel is currently still always the root of the tree.

Based on another PR, so the diff is bigg

@kekekeks kekekeks changed the base branch from master to feature/composition-update-refactor-7 February 6, 2026 10:43
Base automatically changed from feature/composition-update-refactor-7 to master February 6, 2026 11:54
@kekekeks kekekeks requested a review from Copilot February 15, 2026 20:00

This comment was marked as outdated.

Copy link
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

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

Pull request overview

Copilot reviewed 104 out of 104 changed files in this pull request and generated 5 comments.

Comments suppressed due to low confidence (1)

src/Avalonia.Controls/TopLevel.cs:665

  • In HandleClosed, _source is disposed before clearing _source.RootVisual, but clearing the root visual currently updates the renderer/composition root and presentation-source attachment. Because the renderer/layout manager are already disposed, this can throw or leave the visual tree attached. Clear/detach the root visual (and unsubscribe renderer events) before disposing the PresentationSource, and avoid disposing the same layout manager twice (it’s already disposed by _source.Dispose()).

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

@kekekeks kekekeks marked this pull request as ready for review February 15, 2026 21:22
@kekekeks kekekeks requested a review from MrJul February 15, 2026 21:22
Copy link
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

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

Pull request overview

Copilot reviewed 109 out of 109 changed files in this pull request and generated 8 comments.


💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment on lines +108 to 115
// TODO: The check for bounds is no longer correct

var root = (InputElement?)element.VisualRoot;
if (root == null)
return true;

// Check if the element is within the visible area of the window
var visibleBounds = new Rect(0, 0, root.Bounds.Width, root.Bounds.Height);
Copy link

Copilot AI Feb 15, 2026

Choose a reason for hiding this comment

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

The TODO comment mentions "The check for bounds is no longer correct" but the implementation still relies on this check. The code now uses element.VisualRoot which may no longer be the TopLevel (as per the PR's design changes), making the bounds check against root.Bounds potentially incorrect. This should either be fixed or documented more clearly with a follow-up issue reference.

Copilot uses AI. Check for mistakes.
Copy link
Member Author

Choose a reason for hiding this comment

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

TODO's from this PR will be addressed in following PRs

Copy link
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

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

Pull request overview

Copilot reviewed 113 out of 113 changed files in this pull request and generated 12 comments.


💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment on lines +73 to +87
public void Dispose()
{
_layoutDiagnosticBridge?.Dispose();
_layoutDiagnosticBridge = null;
LayoutManager.Dispose();
Renderer.SceneInvalidated -= SceneInvalidated;
// We need to wait for the renderer to complete any in-flight operations
Renderer.Dispose();

PlatformImpl = null;
_pointerOverPreProcessor?.OnCompleted();
_pointerOverPreProcessorSubscription?.Dispose();
if (((IInputRoot)this).PointerOverElement is AvaloniaObject pointerOverElement)
pointerOverElement.PropertyChanged -= PointerOverElement_PropertyChanged;
}
Copy link

Copilot AI Feb 15, 2026

Choose a reason for hiding this comment

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

The PresentationSource.Dispose() method does not set RootVisual to null or call SetPresentationSourceForRootVisual(null) on the current RootVisual. This means that after disposal, the RootVisual may still have a reference to this disposed PresentationSource. This could lead to issues if code tries to access the PresentationSource after disposal. The Dispose method should detach the RootVisual before disposing.

Copilot uses AI. Check for mistakes.
Copy link
Member Author

Choose a reason for hiding this comment

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

This makes sense, but might currently break TopLevel's assumption about event order, i. e. I think it expects to detach the logical tree before visual tree.

Will need to think more about this in further PRs

Comment on lines +46 to 49
else if (e.Root.RootElement == _tipControl?.VisualRoot)
{
_lastWindowEventTime = pointerEvent.Timestamp;
}
Copy link

Copilot AI Feb 15, 2026

Choose a reason for hiding this comment

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

The comparison on line 46 checks if e.Root.RootElement equals _tipControl?.VisualRoot. However, RootElement is an InputElement (from IInputRoot.RootElement) while VisualRoot is a Visual? (from the VisualRoot property). These might not be reference-equal even when they represent the same logical root, since RootElement could be a different type. This comparison should likely use TopLevel.GetTopLevel or compare PresentationSource instances instead.

Copilot uses AI. Check for mistakes.
@@ -49,20 +49,29 @@ protected override void OnAttachedToVisualTree(VisualTreeAttachmentEventArgs e)
{
base.OnAttachedToVisualTree(e);
Copy link
Member Author

Choose a reason for hiding this comment

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

This change set seems unrelated, but this code started to trigger compilation errors for me for some reason, so fixed along the way.

@avaloniaui-bot
Copy link

You can test this PR using the following package version. 12.0.999-cibuild0062267-alpha. (feed url: https://nuget-feed-all.avaloniaui.net/v3/index.json) [PRBUILDID]

@kekekeks
Copy link
Member Author

GH shows commits out of order, 34ac9c9 (the one with green tests) is actually the latest one. This is ready for review, I think

@kekekeks
Copy link
Member Author

I have some notes for later. Those aren't super required to be addressed with this PR since TopLevel still always sits at the root of the visual tree.

  • WindowBase/EmbeddableTopLevelRoot AutomationPeer check Owner == VisualRoot
  • The PresentationSource.Dispose() method does not set RootVisual to null or call SetPresentationSourceForRootVisual(null) on the current RootVisual. This is currently done in TopLevel to preserve the event ordering. Should probably move to PresentationSource for consistency
  • XYFocus.FindElements.cs check for bounds won't be correct if TopLevel isn't the root element
  • Need to do something about X11Window and managed file dialogs, those currently assume that IInputRoot.RootElement is a Window

var root = visual.VisualRoot ??
throw new ArgumentException("Control does not belong to a visual tree.", nameof(visual));
var source = visual.PresentationSource;
var root = source?.RootVisual ??
Copy link
Member

Choose a reason for hiding this comment

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

Nit: the cast below can now be removed.

{
Parent = parent ?? throw new ArgumentNullException(nameof(parent));
Root = root ?? throw new ArgumentNullException(nameof(root));
Debug.Assert(presentationSource.RootVisual != null);
Copy link
Member

Choose a reason for hiding this comment

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

I think the Debug.Assert isn't enough here: this constructor is public. We should check and throw if the presentation source does not have a root visual.

Copy link
Member Author

Choose a reason for hiding this comment

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

Should we make the ctor private?

Copy link
Member

Choose a reason for hiding this comment

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

I don't think so, let's keep the event args constructible. Checking the state should be enough.

/// The root visual of the tree this visual is being attached to or detached from.
/// This is guaranteed to be non-null and will be the same as <see cref="IPresentationSource.RootVisual"/>.
/// </summary>
public Visual RootVisual => PresentationSource.RootVisual!;
Copy link
Member

Choose a reason for hiding this comment

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

Consider snapshotting the root visual in a field in the constructor. This would allow the event args to be stable even if RootVisual is accessed at a later point (where it could be null).

RawDragEvent rawEvent = new RawDragEvent(_dragDrop, type, root, pt, _draggedData!, _allowedEffects, modifiers);
var tl = (root as Visual)?.GetSelfAndVisualAncestors().OfType<TopLevel>().FirstOrDefault();
tl?.PlatformImpl?.Input?.Invoke(rawEvent);
var source = (PresentationSource?)root.RootElement.PresentationSource;
Copy link
Member

Choose a reason for hiding this comment

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

Avoid this cast if possible, as PresentationSource looks safe for the call below.

Copy link
Member Author

Choose a reason for hiding this comment

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

It's casting from IPresentationSource to PresentationSource, what is wrong with that?

Copy link
Member

Choose a reason for hiding this comment

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

Not assuming the impl, and simply being safe when everything in this chain is?

void TakeFocus();

[Obsolete("For unit tests only")]
internal IKeyboardNavigationHandler Tests_KeyboardNavigationHandler { get; }
Copy link
Member

Choose a reason for hiding this comment

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

Remove this to keep the interface clean. There's only one use of it in PopupTests where we control the IPopupHost implementation: we can cast to the correct type there.

protected virtual Size Measure(Size constraint)
{
var l = (Layoutable) InputRoot!;
var l = (Layoutable)InputRoot!.RootElement!;
Copy link
Member

Choose a reason for hiding this comment

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

The Layoutable cast isn't necessary anymore and RootElement is never null.

Copy link
Member Author

Choose a reason for hiding this comment

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

Note that RootElement will be nullable in subsequent PRs.

var bounds = Bounds;
// Native window is not rendered by Avalonia
var transformToVisual = this.TransformToVisual(_currentRoot);
var transformToVisual = _currentRoot.RootVisual != null ? this.TransformToVisual(_currentRoot.RootVisual) : null;
Copy link
Member

Choose a reason for hiding this comment

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

Note: PresentationSource.RootVisual is not nullable, but its implementation accepts null. I read the comment there about keeping it not nullable for now. However, this becomes confusing here: should we expose an additional nullable property for now?


[MemberNotNull(nameof(_groupManager))]
private void EnsureRadioGroupManager(IRenderRoot? root = null)
private void EnsureRadioGroupManager(IPresentationSource? source = null)
Copy link
Member

Choose a reason for hiding this comment

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

The parameter should probably be a root visual. Managing the group should have nothing to do with the presentation source.

Copy link
Member Author

Choose a reason for hiding this comment

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

It was previously managed per-toplevel, so there is no behavior change here.

Copy link
Member

Choose a reason for hiding this comment

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

It's not about the behavioral change, but rather the usage: since the group is scoped to a visual, the presentation source feels like the wrong layer here.

Copy link
Member Author

Choose a reason for hiding this comment

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

Previously RadioGroupManager was only associated with attached trees, I think it's better to keep it as is

@avaloniaui-bot
Copy link

You can test this PR using the following package version. 12.0.999-cibuild0062377-alpha. (feed url: https://nuget-feed-all.avaloniaui.net/v3/index.json) [PRBUILDID]

@avaloniaui-bot
Copy link

You can test this PR using the following package version. 12.0.999-cibuild0062388-alpha. (feed url: https://nuget-feed-all.avaloniaui.net/v3/index.json) [PRBUILDID]

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants

Comments