From 1f87eda59597e2e914cfab8f5512c29e15f6326d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Wies=C5=82aw=20=C5=A0olt=C3=A9s?= Date: Tue, 14 Jul 2026 21:31:34 +0200 Subject: [PATCH 1/2] Fix Browser theme MDI chrome and split drops --- samples/BrowserTabTheme/App.axaml.cs | 5 ++++- samples/BrowserTabTheme/MainWindow.axaml | 5 ++++- samples/BrowserTabTheme/README.md | 6 +++++- .../Styles/BrowserTabAccents.axaml | 4 +++- .../DocumentTabStripItemAndThemeTests.cs | 15 +++++++++++++++ 5 files changed, 31 insertions(+), 4 deletions(-) diff --git a/samples/BrowserTabTheme/App.axaml.cs b/samples/BrowserTabTheme/App.axaml.cs index e1c9cca1a..a695034cd 100644 --- a/samples/BrowserTabTheme/App.axaml.cs +++ b/samples/BrowserTabTheme/App.axaml.cs @@ -66,7 +66,10 @@ public override void OnFrameworkInitializationCompleted() factory.InitLayout(rootDock); - var mainWindow = new MainWindow(); + var mainWindow = new MainWindow + { + DataContext = documentDock + }; mainWindow.DockControl.Factory = factory; mainWindow.DockControl.Layout = rootDock; mainWindow.DockControl.InitializeFactory = true; diff --git a/samples/BrowserTabTheme/MainWindow.axaml b/samples/BrowserTabTheme/MainWindow.axaml index 598998b09..ce0a827de 100644 --- a/samples/BrowserTabTheme/MainWindow.axaml +++ b/samples/BrowserTabTheme/MainWindow.axaml @@ -1,11 +1,14 @@ diff --git a/samples/BrowserTabTheme/README.md b/samples/BrowserTabTheme/README.md index 337efd0cc..af8fbdd8a 100644 --- a/samples/BrowserTabTheme/README.md +++ b/samples/BrowserTabTheme/README.md @@ -6,7 +6,11 @@ This sample backports a browser-tab visual style from StackWich into the origina - Browser-like document and tool tab visuals. - Browser-style window chrome driven by Avalonia drawn decorations and Dock theme resources. -- Dock drag/drop, float, pin, and document creation behavior preserved. +- Dock drag/drop, including dropping a tab onto a side target to create a new + horizontal or vertical document dock. +- Native main-window chrome while document layout mode is MDI, keeping maximized + MDI children clear of the operating-system caption controls. +- Float, pin, and document creation behavior preserved. - Theme dictionaries for both Light (`Default`) and Dark variants. ## What is intentionally excluded diff --git a/src/Dock.Avalonia.Themes.Browser/Styles/BrowserTabAccents.axaml b/src/Dock.Avalonia.Themes.Browser/Styles/BrowserTabAccents.axaml index 4c0b15f7e..ec2495c34 100644 --- a/src/Dock.Avalonia.Themes.Browser/Styles/BrowserTabAccents.axaml +++ b/src/Dock.Avalonia.Themes.Browser/Styles/BrowserTabAccents.axaml @@ -165,7 +165,9 @@ False True True - True + + False False 6 diff --git a/tests/Dock.Avalonia.Themes.UnitTests/DocumentTabStripItemAndThemeTests.cs b/tests/Dock.Avalonia.Themes.UnitTests/DocumentTabStripItemAndThemeTests.cs index a42d8224c..0feabd05b 100644 --- a/tests/Dock.Avalonia.Themes.UnitTests/DocumentTabStripItemAndThemeTests.cs +++ b/tests/Dock.Avalonia.Themes.UnitTests/DocumentTabStripItemAndThemeTests.cs @@ -1,6 +1,8 @@ +using Avalonia.Controls; using Avalonia.Headless.XUnit; using Avalonia.Styling; using Dock.Avalonia.Controls; +using Dock.Avalonia.Themes.Browser; using Dock.Avalonia.Themes.Fluent; using Dock.Avalonia.Themes.Simple; using Xunit; @@ -36,4 +38,17 @@ public void DockSimpleTheme_Can_Instantiate() Styles theme = new DockSimpleTheme(); Assert.NotNull(theme); } + + [AvaloniaFact] + public void BrowserTabTheme_Enables_Full_Document_Dock_Selector() + { + var theme = new BrowserTabTheme(); + var resourceNode = Assert.IsAssignableFrom(theme); + + Assert.True(resourceNode.TryGetResource( + "DockDocumentControlShowDockIndicatorOnly", + ThemeVariant.Default, + out var resource)); + Assert.False(Assert.IsType(resource)); + } } From 8153a26bc05c2b961a0d9f72dda3143257f7137c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Wies=C5=82aw=20=C5=A0olt=C3=A9s?= Date: Tue, 14 Jul 2026 23:29:02 +0200 Subject: [PATCH 2/2] Address browser chrome review feedback --- samples/BrowserTabTheme/App.axaml.cs | 17 +- .../BrowserTabTheme/BrowserTabTheme.csproj | 9 +- samples/BrowserTabTheme/MainWindow.axaml | 14 +- .../BrowserTabTheme/MainWindowViewModel.cs | 155 ++++++++++++++++++ ...BrowserTabThemeMainWindowViewModelTests.cs | 49 ++++++ .../Dock.Avalonia.HeadlessTests.csproj | 1 + 6 files changed, 233 insertions(+), 12 deletions(-) create mode 100644 samples/BrowserTabTheme/MainWindowViewModel.cs create mode 100644 tests/Dock.Avalonia.HeadlessTests/BrowserTabThemeMainWindowViewModelTests.cs diff --git a/samples/BrowserTabTheme/App.axaml.cs b/samples/BrowserTabTheme/App.axaml.cs index a695034cd..193250d1a 100644 --- a/samples/BrowserTabTheme/App.axaml.cs +++ b/samples/BrowserTabTheme/App.axaml.cs @@ -10,6 +10,8 @@ namespace BrowserTabTheme; public partial class App : Application { + private MainWindowViewModel? _mainWindowViewModel; + public override void Initialize() { #if DOCK_USE_GENERATED_APP_INITIALIZE_COMPONENT @@ -66,18 +68,21 @@ public override void OnFrameworkInitializationCompleted() factory.InitLayout(rootDock); + _mainWindowViewModel = new MainWindowViewModel(factory, rootDock); var mainWindow = new MainWindow { - DataContext = documentDock + DataContext = _mainWindowViewModel }; - mainWindow.DockControl.Factory = factory; - mainWindow.DockControl.Layout = rootDock; - mainWindow.DockControl.InitializeFactory = true; - mainWindow.DockControl.InitializeLayout = false; - desktop.MainWindow = mainWindow; + desktop.Exit += (_, _) => DisposeMainWindowViewModel(); } base.OnFrameworkInitializationCompleted(); } + + private void DisposeMainWindowViewModel() + { + _mainWindowViewModel?.Dispose(); + _mainWindowViewModel = null; + } } diff --git a/samples/BrowserTabTheme/BrowserTabTheme.csproj b/samples/BrowserTabTheme/BrowserTabTheme.csproj index 34afb3071..7bff107fd 100644 --- a/samples/BrowserTabTheme/BrowserTabTheme.csproj +++ b/samples/BrowserTabTheme/BrowserTabTheme.csproj @@ -16,6 +16,7 @@ + @@ -25,5 +26,11 @@ - + + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + + + diff --git a/samples/BrowserTabTheme/MainWindow.axaml b/samples/BrowserTabTheme/MainWindow.axaml index ce0a827de..4a487570a 100644 --- a/samples/BrowserTabTheme/MainWindow.axaml +++ b/samples/BrowserTabTheme/MainWindow.axaml @@ -1,16 +1,20 @@ - + diff --git a/samples/BrowserTabTheme/MainWindowViewModel.cs b/samples/BrowserTabTheme/MainWindowViewModel.cs new file mode 100644 index 000000000..805bcdf78 --- /dev/null +++ b/samples/BrowserTabTheme/MainWindowViewModel.cs @@ -0,0 +1,155 @@ +// Copyright (c) Wiesław Šoltés. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for details. + +using System; +using System.Collections.Generic; +using System.Collections.Specialized; +using System.ComponentModel; +using Dock.Model.Controls; +using Dock.Model.Core; +using ReactiveUI; +using ReactiveUI.SourceGenerators; + +namespace BrowserTabTheme; + +/// +/// Coordinates the browser sample layout and its main-window chrome state. +/// +public sealed partial class MainWindowViewModel : ReactiveObject, IDisposable +{ + private readonly HashSet _documentDocks = new(); + private readonly HashSet _dockCollections = new(); + private bool _isDisposed; + + /// + /// Initializes a new instance of the class. + /// + /// The layout factory. + /// The main root layout. + public MainWindowViewModel(IFactory factory, IRootDock layout) + { + Factory = factory ?? throw new ArgumentNullException(nameof(factory)); + Layout = layout ?? throw new ArgumentNullException(nameof(layout)); + ExtendClientAreaToDecorationsHint = true; + RefreshSubscriptions(); + } + + /// + /// Gets the layout factory used by the dock control. + /// + public IFactory Factory { get; } + + /// + /// Gets the main root layout. + /// + public IRootDock Layout { get; } + + /// + /// Gets whether content should extend into the main window decorations. + /// + [Reactive] + public partial bool ExtendClientAreaToDecorationsHint { get; private set; } + + /// + public void Dispose() + { + if (_isDisposed) + { + return; + } + + _isDisposed = true; + ClearSubscriptions(); + } + + private void RefreshSubscriptions() + { + ClearSubscriptions(); + + var pending = new Stack(); + var visited = new HashSet(); + pending.Push(Layout); + + while (pending.Count > 0) + { + var dockable = pending.Pop(); + if (!visited.Add(dockable)) + { + continue; + } + + if (dockable is IDocumentDock documentDock) + { + _documentDocks.Add(documentDock); + if (documentDock is INotifyPropertyChanged notifyingDocumentDock) + { + notifyingDocumentDock.PropertyChanged += OnDocumentDockPropertyChanged; + } + } + + if (dockable is not IDock { VisibleDockables: { } visibleDockables }) + { + continue; + } + + if (visibleDockables is INotifyCollectionChanged notifyingCollection + && _dockCollections.Add(notifyingCollection)) + { + notifyingCollection.CollectionChanged += OnDockCollectionChanged; + } + + for (var index = visibleDockables.Count - 1; index >= 0; index--) + { + pending.Push(visibleDockables[index]); + } + } + + UpdateChromeState(); + } + + private void ClearSubscriptions() + { + foreach (var documentDock in _documentDocks) + { + if (documentDock is INotifyPropertyChanged notifyingDocumentDock) + { + notifyingDocumentDock.PropertyChanged -= OnDocumentDockPropertyChanged; + } + } + + foreach (var dockCollection in _dockCollections) + { + dockCollection.CollectionChanged -= OnDockCollectionChanged; + } + + _documentDocks.Clear(); + _dockCollections.Clear(); + } + + private void OnDockCollectionChanged(object? sender, NotifyCollectionChangedEventArgs e) + { + RefreshSubscriptions(); + } + + private void OnDocumentDockPropertyChanged(object? sender, PropertyChangedEventArgs e) + { + if (string.IsNullOrEmpty(e.PropertyName) || e.PropertyName == nameof(IDocumentDock.LayoutMode)) + { + UpdateChromeState(); + } + } + + private void UpdateChromeState() + { + foreach (var documentDock in _documentDocks) + { + if (documentDock.LayoutMode == DocumentLayoutMode.Mdi) + { + ExtendClientAreaToDecorationsHint = false; + return; + } + } + + ExtendClientAreaToDecorationsHint = true; + } +} diff --git a/tests/Dock.Avalonia.HeadlessTests/BrowserTabThemeMainWindowViewModelTests.cs b/tests/Dock.Avalonia.HeadlessTests/BrowserTabThemeMainWindowViewModelTests.cs new file mode 100644 index 000000000..ff8f646d4 --- /dev/null +++ b/tests/Dock.Avalonia.HeadlessTests/BrowserTabThemeMainWindowViewModelTests.cs @@ -0,0 +1,49 @@ +// Copyright (c) Wiesław Šoltés. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for details. + +using BrowserTabTheme; +using Dock.Model.Controls; +using Dock.Model.Core; +using Dock.Model.Mvvm; +using Xunit; + +namespace Dock.Avalonia.HeadlessTests; + +public class BrowserTabThemeMainWindowViewModelTests +{ + [Fact] + public void ChromeStateTracksAllCurrentDocumentDocks() + { + var factory = new Factory(); + var root = factory.CreateRootDock(); + root.VisibleDockables = factory.CreateList(); + var proportionalDock = factory.CreateProportionalDock(); + proportionalDock.VisibleDockables = factory.CreateList(); + var firstDocumentDock = factory.CreateDocumentDock(); + firstDocumentDock.VisibleDockables = factory.CreateList(); + + factory.AddDockable(proportionalDock, firstDocumentDock); + factory.AddDockable(root, proportionalDock); + factory.InitLayout(root); + + using var viewModel = new MainWindowViewModel(factory, root); + + Assert.True(viewModel.ExtendClientAreaToDecorationsHint); + + var secondDocumentDock = factory.CreateDocumentDock(); + secondDocumentDock.VisibleDockables = factory.CreateList(); + factory.AddDockable(proportionalDock, secondDocumentDock); + secondDocumentDock.LayoutMode = DocumentLayoutMode.Mdi; + + Assert.False(viewModel.ExtendClientAreaToDecorationsHint); + + firstDocumentDock.LayoutMode = DocumentLayoutMode.Mdi; + factory.RemoveDockable(secondDocumentDock, false); + + Assert.False(viewModel.ExtendClientAreaToDecorationsHint); + + firstDocumentDock.LayoutMode = DocumentLayoutMode.Tabbed; + + Assert.True(viewModel.ExtendClientAreaToDecorationsHint); + } +} diff --git a/tests/Dock.Avalonia.HeadlessTests/Dock.Avalonia.HeadlessTests.csproj b/tests/Dock.Avalonia.HeadlessTests/Dock.Avalonia.HeadlessTests.csproj index e4fa18fa4..5a254931a 100644 --- a/tests/Dock.Avalonia.HeadlessTests/Dock.Avalonia.HeadlessTests.csproj +++ b/tests/Dock.Avalonia.HeadlessTests/Dock.Avalonia.HeadlessTests.csproj @@ -16,6 +16,7 @@ +