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; } } }