using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Diagnostics;
using Avalonia.Reactive;
using Avalonia.Controls.Metadata;
using Avalonia.Controls.Platform;
using Avalonia.Controls.Primitives;
using Avalonia.Data;
using Avalonia.Input;
using Avalonia.Input.Raw;
using Avalonia.Input.TextInput;
using Avalonia.Interactivity;
using Avalonia.Layout;
using Avalonia.Logging;
using Avalonia.LogicalTree;
using Avalonia.Media;
using Avalonia.Platform;
using Avalonia.Platform.Storage;
using Avalonia.Rendering;
using Avalonia.Styling;
using Avalonia.Utilities;
using Avalonia.Input.Platform;
using System.Linq;
using System.Threading.Tasks;
using Avalonia.Diagnostics;
using Avalonia.Rendering.Composition;
using Avalonia.Threading;
namespace Avalonia.Controls
{
///
/// Base class for top-level widgets.
///
///
/// This class acts as a base for top level widget.
/// It handles scheduling layout, styling and rendering as well as
/// tracking the widget's .
///
[TemplatePart("PART_TransparencyFallback", typeof(Border))]
public abstract class TopLevel : ContentControl,
ICloseable,
IStyleHost,
ILogicalRoot
{
///
/// Defines the property.
///
public static readonly DirectProperty ClientSizeProperty =
AvaloniaProperty.RegisterDirect(nameof(ClientSize), o => o.ClientSize);
///
/// Defines the property.
///
public static readonly DirectProperty FrameSizeProperty =
AvaloniaProperty.RegisterDirect(nameof(FrameSize), o => o.FrameSize);
///
/// Defines the property.
///
public static readonly StyledProperty> TransparencyLevelHintProperty =
AvaloniaProperty.Register>(nameof(TransparencyLevelHint), Array.Empty());
///
/// Defines the property.
///
public static readonly DirectProperty ActualTransparencyLevelProperty =
AvaloniaProperty.RegisterDirect(nameof(ActualTransparencyLevel),
o => o.ActualTransparencyLevel,
unsetValue: WindowTransparencyLevel.None);
///
/// Defines the property.
///
public static readonly StyledProperty TransparencyBackgroundFallbackProperty =
AvaloniaProperty.Register(nameof(TransparencyBackgroundFallback), Brushes.White);
///
public static readonly StyledProperty ActualThemeVariantProperty =
ThemeVariantScope.ActualThemeVariantProperty.AddOwner();
///
public static readonly StyledProperty RequestedThemeVariantProperty =
ThemeVariantScope.RequestedThemeVariantProperty.AddOwner();
///
/// Defines the SystemBarColor attached property.
///
public static readonly AttachedProperty SystemBarColorProperty =
AvaloniaProperty.RegisterAttached(
"SystemBarColor",
inherits: true);
///
/// Defines the AutoSafeAreaPadding attached property.
///
public static readonly AttachedProperty AutoSafeAreaPaddingProperty =
AvaloniaProperty.RegisterAttached(
"AutoSafeAreaPadding",
defaultValue: true);
///
/// Defines the event.
///
public static readonly RoutedEvent BackRequestedEvent =
RoutedEvent.Register(nameof(BackRequested), RoutingStrategies.Bubble);
private static readonly WeakEvent
ResourcesChangedWeakEvent = WeakEvent.Register(
(s, h) => s.ResourcesChanged += h,
(s, h) => s.ResourcesChanged -= h
);
private readonly IInputManager? _inputManager;
private readonly IToolTipService? _tooltipService;
private readonly IAccessKeyHandler? _accessKeyHandler;
private readonly IKeyboardNavigationHandler? _keyboardNavigationHandler;
internal IKeyboardNavigationHandler Tests_KeyboardNavigationHandler => _keyboardNavigationHandler!;
private readonly IGlobalStyles? _globalStyles;
private readonly IThemeVariantHost? _applicationThemeHost;
private readonly IDisposable? _backGestureSubscription;
private readonly Dictionary _platformImplBindings = new();
private double _scaling;
private Size _clientSize;
private Size? _frameSize;
private WindowTransparencyLevel _actualTransparencyLevel;
private Border? _transparencyFallbackBorder;
private TargetWeakEventSubscriber? _resourcesChangesSubscriber;
private IStorageProvider? _storageProvider;
private Screens? _screens;
private readonly PresentationSource _source;
private readonly TopLevelHost _topLevelHost;
internal TopLevelHost TopLevelHost => _topLevelHost;
internal new PresentationSource PresentationSource => _source;
internal IInputRoot InputRoot => _source;
///
/// Initializes static members of the class.
///
static TopLevel()
{
KeyboardNavigation.TabNavigationProperty.OverrideDefaultValue(KeyboardNavigationMode.Cycle);
Avalonia.Styling.Container.SizingProperty.OverrideDefaultValue(ContainerSizing.WidthAndHeight);
AffectsMeasure(ClientSizeProperty);
SystemBarColorProperty.Changed.AddClassHandler((view, e) =>
{
if (e.NewValue is SolidColorBrush colorBrush)
{
if (view.Parent is TopLevel tl && tl.InsetsManager is { } insetsManager)
{
insetsManager.SystemBarColor = colorBrush.Color;
}
if (view is TopLevel topLevel && topLevel.InsetsManager is { } insets)
{
insets.SystemBarColor = colorBrush.Color;
}
}
});
AutoSafeAreaPaddingProperty.Changed.AddClassHandler((view, e) =>
{
var topLevel = view as TopLevel ?? view.Parent as TopLevel;
topLevel?.InvalidateChildInsetsPadding();
});
ToolTip.ServiceEnabledProperty.Changed.Subscribe(OnToolTipServiceEnabledChanged);
}
///
/// Initializes a new instance of the class.
///
/// The platform-specific window implementation.
public TopLevel(ITopLevelImpl impl)
: this(impl, AvaloniaLocator.Current)
{
}
///
/// Initializes a new instance of the class.
///
/// The platform-specific window implementation.
///
/// The dependency resolver to use. If null the default dependency resolver will be used.
///
internal TopLevel(ITopLevelImpl impl, IAvaloniaDependencyResolver? dependencyResolver)
{
PlatformImpl = impl ?? throw new InvalidOperationException(
"Could not create window implementation: maybe no windowing subsystem was initialized?");
dependencyResolver ??= AvaloniaLocator.Current;
var hostVisual = new TopLevelHost(this);
_topLevelHost = hostVisual;
((ISetLogicalParent)hostVisual).SetParent(this);
LogicalChildren.Add(hostVisual);
_source = new PresentationSource(hostVisual, this,
impl, dependencyResolver, () => ClientSize);
_source.Renderer.SceneInvalidated += SceneInvalidated;
_scaling = ValidateScaling(impl.RenderScaling);
_actualTransparencyLevel = PlatformImpl.TransparencyLevel;
_accessKeyHandler = TryGetService(dependencyResolver);
_inputManager = TryGetService(dependencyResolver);
_tooltipService = TryGetService(dependencyResolver);
_keyboardNavigationHandler = TryGetService(dependencyResolver);
_globalStyles = TryGetService(dependencyResolver);
_applicationThemeHost = TryGetService(dependencyResolver);
impl.Closed = HandleClosed;
impl.Paint = HandlePaint;
impl.Resized = HandleResized;
impl.ScalingChanged = HandleScalingChanged;
impl.TransparencyLevelChanged = HandleTransparencyLevelChanged;
CreatePlatformImplBinding(TransparencyLevelHintProperty, hint => PlatformImpl.SetTransparencyLevelHint(hint ?? Array.Empty()));
CreatePlatformImplBinding(ActualThemeVariantProperty, variant =>
{
variant ??= ThemeVariant.Default;
PlatformImpl?.SetFrameThemeVariant((PlatformThemeVariant?)variant ?? PlatformThemeVariant.Light);
});
_keyboardNavigationHandler?.SetOwner(this);
_accessKeyHandler?.SetOwner(this);
if (_globalStyles is object)
{
_globalStyles.GlobalStylesAdded += ((IStyleHost)this).StylesAdded;
_globalStyles.GlobalStylesRemoved += ((IStyleHost)this).StylesRemoved;
}
if (_applicationThemeHost is { })
{
SetValue(ActualThemeVariantProperty, _applicationThemeHost.ActualThemeVariant, BindingPriority.Template);
_applicationThemeHost.ActualThemeVariantChanged += GlobalActualThemeVariantChanged;
}
ClientSize = impl.ClientSize;
var stylingParent = ((IStyleHost)this).StylingParent;
if (stylingParent is IResourceHost applicationResources2)
{
_resourcesChangesSubscriber = new TargetWeakEventSubscriber(
this, static (target, _, _, token) =>
{
target.NotifyResourcesChanged(token);
});
ResourcesChangedWeakEvent.Subscribe(applicationResources2, _resourcesChangesSubscriber);
}
impl.LostFocus += PlatformImpl_LostFocus;
if (impl.TryGetFeature() is { } systemNavigationManager)
{
systemNavigationManager.BackRequested += (_, e) =>
{
e.RoutedEvent = BackRequestedEvent;
Dispatcher.UIThread.Send(_ => RaiseEvent(e));
};
}
_backGestureSubscription = _inputManager?.PreProcess.Subscribe(e =>
{
if (e.Root != this)
return;
bool backRequested = false;
if (e is RawKeyEventArgs rawKeyEventArgs && rawKeyEventArgs.Type == RawKeyEventType.KeyDown)
{
var keymap = PlatformSettings?.HotkeyConfiguration.Back;
if (keymap != null)
{
var keyEvent = new KeyEventArgs()
{
KeyModifiers = (KeyModifiers)rawKeyEventArgs.Modifiers,
Key = rawKeyEventArgs.Key,
PhysicalKey = rawKeyEventArgs.PhysicalKey,
KeyDeviceType = rawKeyEventArgs.KeyDeviceType,
KeySymbol = rawKeyEventArgs.KeySymbol
};
backRequested = keymap.Any(key => key.Matches(keyEvent));
}
}
else if (e is RawPointerEventArgs pointerEventArgs)
{
backRequested = pointerEventArgs.Type == RawPointerEventType.XButton1Down;
}
if (backRequested)
{
var backRequestedEventArgs = new RoutedEventArgs(BackRequestedEvent);
Dispatcher.UIThread.Send(_ => RaiseEvent(backRequestedEventArgs));
e.Handled = backRequestedEventArgs.Handled;
}
});
}
///
/// Fired when the window is opened.
///
public event EventHandler? Opened;
///
/// Fired when the window is closed.
///
public event EventHandler? Closed;
///
/// Gets or sets a method called when the TopLevel's scaling changes.
///
public event EventHandler? ScalingChanged;
///
/// Gets or sets the client size of the window.
///
public Size ClientSize
{
get => _clientSize;
protected set => SetAndRaise(ClientSizeProperty, ref _clientSize, value);
}
///
/// Gets or sets the total size of the window.
///
public Size? FrameSize
{
get => _frameSize;
protected set => SetAndRaise(FrameSizeProperty, ref _frameSize, value);
}
///
/// Gets or sets the that the TopLevel should use when possible.
/// Accepts multiple values which are applied in a fallback order.
/// For instance, with "Mica, Blur" Mica will be applied only on platforms where it is possible,
/// and Blur will be used on the rest of them. Default value is an empty array or "None".
///
public IReadOnlyList TransparencyLevelHint
{
get => GetValue(TransparencyLevelHintProperty);
set => SetValue(TransparencyLevelHintProperty, value);
}
///
/// Gets the achieved that the platform was able to provide.
///
public WindowTransparencyLevel ActualTransparencyLevel
{
get => _actualTransparencyLevel;
private set => SetAndRaise(ActualTransparencyLevelProperty, ref _actualTransparencyLevel, value);
}
///
/// Gets or sets the that transparency will blend with when transparency is not supported.
/// By default this is a solid white brush.
///
public IBrush TransparencyBackgroundFallback
{
get => GetValue(TransparencyBackgroundFallbackProperty);
set => SetValue(TransparencyBackgroundFallbackProperty, value);
}
///
public ThemeVariant? RequestedThemeVariant
{
get => GetValue(RequestedThemeVariantProperty);
set => SetValue(RequestedThemeVariantProperty, value);
}
///
/// Occurs when physical Back Button is pressed or a back navigation has been requested.
///
public event EventHandler BackRequested
{
add => AddHandler(BackRequestedEvent, value);
remove => RemoveHandler(BackRequestedEvent, value);
}
internal ILayoutManager LayoutManager => _source.LayoutManager;
///
/// Gets the platform-specific window implementation.
///
public ITopLevelImpl? PlatformImpl { get; private set; }
///
/// Trys to get the platform handle for the TopLevel-derived control.
///
///
/// An describing the window handle, or null if the handle
/// could not be retrieved.
///
public IPlatformHandle? TryGetPlatformHandle() => PlatformImpl?.Handle;
private protected void CreatePlatformImplBinding(StyledProperty property, Action onValue)
{
_platformImplBindings.TryGetValue(property, out var actions);
_platformImplBindings[property] = actions + UpdatePlatformImpl;
UpdatePlatformImpl(); // execute the action now to handle the default value, which may have been overridden
void UpdatePlatformImpl()
{
if (PlatformImpl is not null)
{
onValue(GetValue(property));
}
}
}
///
/// Gets the renderer for the window.
///
internal CompositingRenderer Renderer => _source.Renderer;
// This property setter is here purely for lazy unit tests
// that don't want to set up a proper hit-testable visual tree
// and should be removed after fixing those tests
internal IHitTester? HitTesterOverride
{
get => _source.HitTesterOverride;
set => _source.HitTesterOverride = value;
}
///
/// Gets a value indicating whether the renderer should draw specific diagnostics.
///
public RendererDiagnostics RendererDiagnostics => Renderer.Diagnostics;
internal PixelPoint? LastPointerPosition => _source.GetLastPointerPosition(this);
///
/// Gets the access key handler for the window.
///
internal IAccessKeyHandler? AccessKeyHandler => _accessKeyHandler;
///
/// Gets or sets the keyboard navigation handler for the window.
///
///
/// Helper for setting the color of the platform's system bars.
///
/// The main view attached to the toplevel, or the toplevel.
/// The color to set.
public static void SetSystemBarColor(Control control, SolidColorBrush? color)
{
control.SetValue(SystemBarColorProperty, color);
}
///
/// Helper for getting the color of the platform's system bars.
///
/// The main view attached to the toplevel, or the toplevel.
/// The current color of the platform's system bars.
public static SolidColorBrush? GetSystemBarColor(Control control)
{
return control.GetValue(SystemBarColorProperty);
}
///
/// Enabled or disables whenever TopLevel should automatically adjust paddings depending on the safe area.
///
/// The main view attached to the toplevel, or the toplevel.
/// Value to be set.
public static void SetAutoSafeAreaPadding(Control control, bool value)
{
control.SetValue(AutoSafeAreaPaddingProperty, value);
}
///
/// Gets if auto safe area padding is enabled.
///
/// The main view attached to the toplevel, or the toplevel.
public static bool GetAutoSafeAreaPadding(Control control)
{
return control.GetValue(AutoSafeAreaPaddingProperty);
}
///
public double RenderScaling => _scaling;
IStyleHost IStyleHost.StylingParent => _globalStyles!;
///
/// File System storage service used for file pickers and bookmarks.
///
public IStorageProvider StorageProvider => _storageProvider
??= AvaloniaLocator.Current.GetService()?.CreateProvider(this)
?? PlatformImpl?.TryGetFeature()
?? new NoopStorageProvider();
public IInsetsManager? InsetsManager => PlatformImpl?.TryGetFeature();
public IInputPane? InputPane => PlatformImpl?.TryGetFeature();
public ILauncher Launcher => PlatformImpl?.TryGetFeature() ?? new NoopLauncher();
///
/// Gets platform screens implementation.
///
public Screens? Screens => _screens ??=
PlatformImpl?.TryGetFeature() is { } screenImpl ? new Screens(screenImpl) : null;
///
/// Gets the platform's clipboard implementation
///
public IClipboard? Clipboard => PlatformImpl?.TryGetFeature();
///
/// Gets focus manager of the root.
///
///
/// Focus manager can be null only if window wasn't initialized yet.
///
public IFocusManager FocusManager => _source.FocusManager;
///
/// Represents a contract for accessing top-level platform-specific settings.
///
///
/// PlatformSettings can be null only if window wasn't initialized yet.
///
// TODO: Un-private
private IPlatformSettings? PlatformSettings => AvaloniaLocator.Current.GetService();
///
/// Gets the for which the given is hosted in.
///
/// The visual to query its TopLevel
/// The TopLevel
public static TopLevel? GetTopLevel(Visual? visual)
{
while (visual != null)
{
if (visual is TopLevel tl)
return tl;
visual = visual.VisualParent;
}
return null;
}
///
/// Requests a to be inhibited.
/// The behavior remains inhibited until the return value is disposed.
/// The available set of s depends on the platform.
/// If a behavior is inhibited on a platform where this type is not supported the request will have no effect.
///
public async Task RequestPlatformInhibition(PlatformInhibitionType type, string reason)
{
var platformBehaviorInhibition = PlatformImpl?.TryGetFeature();
if (platformBehaviorInhibition == null)
{
return Disposable.Create(() => { });
}
switch (type)
{
case PlatformInhibitionType.AppSleep:
await platformBehaviorInhibition.SetInhibitAppSleep(true, reason);
return Disposable.Create(() => platformBehaviorInhibition.SetInhibitAppSleep(false, reason).Wait());
default:
return Disposable.Create(() => { });
}
}
///
/// Enqueues a callback to be called on the next animation tick
///
public void RequestAnimationFrame(Action action)
{
Dispatcher.UIThread.VerifyAccess();
MediaContext.Instance.RequestAnimationFrame(action);
}
protected override void OnPropertyChanged(AvaloniaPropertyChangedEventArgs change)
{
base.OnPropertyChanged(change);
if (change.Property == ContentProperty)
{
InvalidateChildInsetsPadding();
}
else if (_platformImplBindings.TryGetValue(change.Property, out var bindingAction))
{
bindingAction();
}
}
private IDisposable? _insetsPaddings;
private void InvalidateChildInsetsPadding()
{
if (Content is Control child
&& InsetsManager is { } insetsManager)
{
insetsManager.SafeAreaChanged -= InsetsManagerOnSafeAreaChanged;
_insetsPaddings?.Dispose();
if (child.GetValue(AutoSafeAreaPaddingProperty))
{
insetsManager.SafeAreaChanged += InsetsManagerOnSafeAreaChanged;
_insetsPaddings = child.SetValue(
PaddingProperty,
insetsManager.SafeAreaPadding,
BindingPriority.Style); // lower priority, so it can be redefined by user
}
void InsetsManagerOnSafeAreaChanged(object? sender, SafeAreaChangedArgs e)
{
InvalidateChildInsetsPadding();
}
}
}
///
/// Handles a paint notification from .
///
/// The dirty area.
private void HandlePaint(Rect rect)
{
Renderer.Paint(rect);
}
private protected void StartRendering() => MediaContext.Instance.AddTopLevel(this, LayoutManager, Renderer);
private protected void StopRendering() => MediaContext.Instance.RemoveTopLevel(this);
///
/// Handles a closed notification from .
///
private protected virtual void HandleClosed()
{
_source.Dispose();
StopRendering();
Debug.Assert(PlatformImpl != null);
// The PlatformImpl is completely invalid at this point
PlatformImpl = null;
_scaling = 1.0;
if (_globalStyles is object)
{
_globalStyles.GlobalStylesAdded -= ((IStyleHost)this).StylesAdded;
_globalStyles.GlobalStylesRemoved -= ((IStyleHost)this).StylesRemoved;
}
if (_applicationThemeHost is { })
{
_applicationThemeHost.ActualThemeVariantChanged -= GlobalActualThemeVariantChanged;
}
_backGestureSubscription?.Dispose();
var logicalArgs = new LogicalTreeAttachmentEventArgs(this, this, null);
((ILogical)this).NotifyDetachedFromLogicalTree(logicalArgs);
_source.RootVisual = null!;
OnClosed(EventArgs.Empty);
LayoutManager.Dispose();
_platformImplBindings.Clear();
}
///
/// Handles a resize notification from .
///
/// The new client size.
/// The reason for the resize.
internal virtual void HandleResized(Size clientSize, WindowResizeReason reason)
{
ClientSize = clientSize;
Width = clientSize.Width;
Height = clientSize.Height;
LayoutManager.ExecuteLayoutPass();
Renderer.Resized(clientSize);
}
///
/// Handles a window scaling change notification from
/// .
///
/// The window scaling.
private void HandleScalingChanged(double scaling)
{
_scaling = ValidateScaling(scaling);
LayoutHelper.InvalidateSelfAndChildrenMeasure(this);
Dispatcher.UIThread.Send(_ => ScalingChanged?.Invoke(this, EventArgs.Empty));
InvalidateChildInsetsPadding();
}
private void HandleTransparencyLevelChanged(WindowTransparencyLevel transparencyLevel)
{
if (_transparencyFallbackBorder != null)
{
if (transparencyLevel == WindowTransparencyLevel.None)
{
_transparencyFallbackBorder.Background = TransparencyBackgroundFallback;
}
else
{
_transparencyFallbackBorder.Background = null;
}
}
ActualTransparencyLevel = transparencyLevel;
}
protected override void OnApplyTemplate(TemplateAppliedEventArgs e)
{
base.OnApplyTemplate(e);
if (PlatformImpl is null)
return;
_transparencyFallbackBorder = e.NameScope.Find("PART_TransparencyFallback");
HandleTransparencyLevelChanged(PlatformImpl.TransparencyLevel);
}
///
/// Raises the event.
///
/// The event args.
protected virtual void OnOpened(EventArgs e)
{
Dispatcher.UIThread.Send(_ => Opened?.Invoke(this, e));
}
///
/// Raises the event.
///
/// The event args.
protected virtual void OnClosed(EventArgs e)
{
Dispatcher.UIThread.Send(_ => Closed?.Invoke(this, e));
}
///
/// Tries to get a service from an , logging a
/// warning if not found.
///
/// The service type.
/// The resolver.
/// The service.
private T? TryGetService(IAvaloniaDependencyResolver resolver) where T : class
{
var result = resolver.GetService();
if (result == null)
{
Logger.TryGet(LogEventLevel.Warning, LogArea.Control)?.Log(
this,
"Could not create {Service} : maybe Application.RegisterServices() wasn't called?",
typeof(T));
}
return result;
}
private void GlobalActualThemeVariantChanged(object? sender, EventArgs e)
{
SetValue(ActualThemeVariantProperty, ((IThemeVariantHost)sender!).ActualThemeVariant, BindingPriority.Template);
}
private void SceneInvalidated(object? sender, SceneInvalidatedEventArgs e)
{
UpdateToolTip(e.DirtyRect);
}
private static void OnToolTipServiceEnabledChanged(AvaloniaPropertyChangedEventArgs args)
{
if (args.GetNewValue()
&& args.Priority != BindingPriority.Inherited
&& args.Sender is Visual visual
&& GetTopLevel(visual) is { } topLevel)
{
topLevel.UpdateToolTip(visual.Bounds.Translate((Vector)visual.TranslatePoint(default, topLevel)!));
}
}
private void UpdateToolTip(Rect dirtyRect)
{
if (_tooltipService != null && IsPointerOver && _source.GetLastPointerPosition(this) is { } lastPos)
{
var clientPoint = this.PointToClient(lastPos);
if (dirtyRect.Contains(clientPoint))
{
_tooltipService.Update(_source, this.InputHitTest(clientPoint, enabledElementsOnly: false) as Visual);
}
}
}
void PlatformImpl_LostFocus()
{
var focused = TopLevel.GetTopLevel((Visual?)FocusManager?.GetFocusedElement());
if (focused == this)
KeyboardDevice.Instance?.SetFocusedElement(null, NavigationMethod.Unspecified, KeyModifiers.None, false);
}
protected override bool BypassFlowDirectionPolicies => true;
protected internal override void InvalidateMirrorTransform()
{
// Do nothing becuase TopLevel should't apply MirrorTransform on himself.
}
private double ValidateScaling(double scaling)
{
if (MathUtilities.IsNegativeOrNonFinite(scaling) || MathUtilities.IsZero(scaling))
{
throw new InvalidOperationException(
$"Invalid {nameof(ITopLevelImpl.RenderScaling)} value {scaling} returned from {PlatformImpl?.GetType()}");
}
if (MathUtilities.IsOne(scaling))
{
// Ensure we've got exactly 1.0 and not an approximation,
// so we don't have to use MathUtilities.IsOne in various layout hot paths.
return 1.0;
}
return scaling;
}
}
}