Browse Source

Merge branch 'master' into fixes/menu-grid-sharedsizescope

pull/4448/head
Steven Kirk 6 years ago
committed by GitHub
parent
commit
f3300ed7e5
No known key found for this signature in database GPG Key ID: 4AEE18F83AFDEB23
  1. 3
      samples/ControlCatalog/Pages/ListBoxPage.xaml
  2. 68
      samples/ControlCatalog/Pages/ListBoxPage.xaml.cs
  3. 65
      samples/ControlCatalog/ViewModels/ListBoxPageViewModel.cs
  4. 7
      src/Avalonia.Controls/ApiCompatBaseline.txt
  5. 7
      src/Avalonia.Controls/AutoCompleteBox.cs
  6. 7
      src/Avalonia.Controls/Calendar/CalendarDatePicker.cs
  7. 22
      src/Avalonia.Controls/ComboBox.cs
  8. 3
      src/Avalonia.Controls/ContextMenu.cs
  9. 4
      src/Avalonia.Controls/IMenuElement.cs
  10. 6
      src/Avalonia.Controls/IMenuItem.cs
  11. 2
      src/Avalonia.Controls/ItemsControl.cs
  12. 3
      src/Avalonia.Controls/ListBox.cs
  13. 5
      src/Avalonia.Controls/Menu.cs
  14. 8
      src/Avalonia.Controls/MenuBase.cs
  15. 16
      src/Avalonia.Controls/MenuItem.cs
  16. 12
      src/Avalonia.Controls/Platform/DefaultMenuInteractionHandler.cs
  17. 61
      src/Avalonia.Controls/Primitives/LightDismissOverlayLayer.cs
  18. 143
      src/Avalonia.Controls/Primitives/Popup.cs
  19. 33
      src/Avalonia.Controls/Primitives/PopupClosedEventArgs.cs
  20. 26
      src/Avalonia.Controls/Primitives/VisualLayerManager.cs
  21. 37
      src/Avalonia.Controls/Utils/SelectedItemsSync.cs
  22. 3
      src/Avalonia.DesignerSupport/DesignWindowLoader.cs
  23. 29
      src/Avalonia.Input/InputExtensions.cs
  24. 3
      src/Avalonia.Themes.Default/Accents/BaseDark.xaml
  25. 3
      src/Avalonia.Themes.Default/Accents/BaseLight.xaml
  26. 2
      src/Avalonia.Themes.Default/AutoCompleteBox.xaml
  27. 2
      src/Avalonia.Themes.Default/ComboBox.xaml
  28. 334
      src/Avalonia.Themes.Default/DatePicker.xaml
  29. 3
      src/Avalonia.Themes.Default/DefaultTheme.xaml
  30. 4
      src/Avalonia.Themes.Default/GridSplitter.xaml
  31. 6
      src/Avalonia.Themes.Default/MenuItem.xaml
  32. 219
      src/Avalonia.Themes.Default/SplitView.xaml
  33. 283
      src/Avalonia.Themes.Default/TimePicker.xaml
  34. 2
      src/Avalonia.Themes.Fluent/AutoCompleteBox.xaml
  35. 2
      src/Avalonia.Themes.Fluent/CalendarDatePicker.xaml
  36. 2
      src/Avalonia.Themes.Fluent/ComboBox.xaml
  37. 2
      src/Avalonia.Themes.Fluent/DatePicker.xaml
  38. 5
      src/Avalonia.Themes.Fluent/MenuItem.xaml
  39. 2
      src/Avalonia.Themes.Fluent/TimePicker.xaml
  40. 79
      src/Avalonia.Visuals/Media/TextFormatting/TextFormatterImpl.cs
  41. 5
      src/Avalonia.Visuals/Media/TextFormatting/TextLayout.cs
  42. 4
      src/Avalonia.Visuals/Media/TextFormatting/TextLineImpl.cs
  43. 11
      src/Avalonia.Visuals/Rendering/ICustomSimpleHitTest.cs
  44. 6
      src/Avalonia.Visuals/Rendering/SceneGraph/Scene.cs
  45. 10
      src/Skia/Avalonia.Skia/TextShaperImpl.cs
  46. 126
      tests/Avalonia.Controls.UnitTests/ListBoxTests_Multiple.cs
  47. 18
      tests/Avalonia.Controls.UnitTests/Platform/DefaultMenuInteractionHandlerTests.cs
  48. 59
      tests/Avalonia.Controls.UnitTests/Primitives/PopupTests.cs
  49. 14
      tests/Avalonia.Controls.UnitTests/Utils/SelectedItemsSyncTests.cs
  50. 1
      tests/Avalonia.UnitTests/MockWindowingPlatform.cs

3
samples/ControlCatalog/Pages/ListBoxPage.xaml

@ -11,8 +11,7 @@
Spacing="16">
<StackPanel Orientation="Vertical" Spacing="8">
<ListBox Items="{Binding Items}"
SelectedItem="{Binding SelectedItem}"
SelectedItems="{Binding SelectedItems}"
Selection="{Binding Selection}"
AutoScrollToSelectedItem="True"
SelectionMode="{Binding SelectionMode}"
Width="250"

68
samples/ControlCatalog/Pages/ListBoxPage.xaml.cs

@ -1,10 +1,6 @@
using System;
using System.Collections.ObjectModel;
using System.Linq;
using System.Reactive;
using Avalonia.Controls;
using Avalonia.Markup.Xaml;
using ReactiveUI;
using ControlCatalog.ViewModels;
namespace ControlCatalog.Pages
{
@ -13,72 +9,12 @@ namespace ControlCatalog.Pages
public ListBoxPage()
{
InitializeComponent();
DataContext = new PageViewModel();
DataContext = new ListBoxPageViewModel();
}
private void InitializeComponent()
{
AvaloniaXamlLoader.Load(this);
}
private class PageViewModel : ReactiveObject
{
private int _counter;
private SelectionMode _selectionMode;
public PageViewModel()
{
Items = new ObservableCollection<string>(Enumerable.Range(1, 10000).Select(i => GenerateItem()));
SelectedItems = new ObservableCollection<string>();
AddItemCommand = ReactiveCommand.Create(() => Items.Add(GenerateItem()));
RemoveItemCommand = ReactiveCommand.Create(() =>
{
while (SelectedItems.Count > 0)
{
Items.Remove(SelectedItems[0]);
}
});
SelectRandomItemCommand = ReactiveCommand.Create(() =>
{
var random = new Random();
SelectedItem = Items[random.Next(Items.Count - 1)];
});
}
public ObservableCollection<string> Items { get; }
private string _selectedItem;
public string SelectedItem
{
get { return _selectedItem; }
set { this.RaiseAndSetIfChanged(ref _selectedItem, value); }
}
public ObservableCollection<string> SelectedItems { get; }
public ReactiveCommand<Unit, Unit> AddItemCommand { get; }
public ReactiveCommand<Unit, Unit> RemoveItemCommand { get; }
public ReactiveCommand<Unit, Unit> SelectRandomItemCommand { get; }
public SelectionMode SelectionMode
{
get => _selectionMode;
set
{
SelectedItems.Clear();
this.RaiseAndSetIfChanged(ref _selectionMode, value);
}
}
private string GenerateItem() => $"Item {_counter++.ToString()}";
}
}
}

65
samples/ControlCatalog/ViewModels/ListBoxPageViewModel.cs

@ -0,0 +1,65 @@
using System;
using System.Collections.ObjectModel;
using System.Linq;
using System.Reactive;
using Avalonia.Controls;
using ReactiveUI;
namespace ControlCatalog.ViewModels
{
public class ListBoxPageViewModel : ReactiveObject
{
private int _counter;
private SelectionMode _selectionMode;
public ListBoxPageViewModel()
{
Items = new ObservableCollection<string>(Enumerable.Range(1, 10000).Select(i => GenerateItem()));
Selection = new SelectionModel();
Selection.Select(1);
AddItemCommand = ReactiveCommand.Create(() => Items.Add(GenerateItem()));
RemoveItemCommand = ReactiveCommand.Create(() =>
{
while (Selection.SelectedItems.Count > 0)
{
Items.Remove((string)Selection.SelectedItems.First());
}
});
SelectRandomItemCommand = ReactiveCommand.Create(() =>
{
var random = new Random();
using (Selection.Update())
{
Selection.ClearSelection();
Selection.Select(random.Next(Items.Count - 1));
}
});
}
public ObservableCollection<string> Items { get; }
public SelectionModel Selection { get; }
public ReactiveCommand<Unit, Unit> AddItemCommand { get; }
public ReactiveCommand<Unit, Unit> RemoveItemCommand { get; }
public ReactiveCommand<Unit, Unit> SelectRandomItemCommand { get; }
public SelectionMode SelectionMode
{
get => _selectionMode;
set
{
Selection.ClearSelection();
this.RaiseAndSetIfChanged(ref _selectionMode, value);
}
}
private string GenerateItem() => $"Item {_counter++.ToString()}";
}
}

7
src/Avalonia.Controls/ApiCompatBaseline.txt

@ -0,0 +1,7 @@
Compat issues with assembly Avalonia.Controls:
MembersMustExist : Member 'protected void Avalonia.Controls.ComboBox.PopupClosedOverride(Avalonia.Controls.Primitives.PopupClosedEventArgs)' does not exist in the implementation but it does exist in the contract.
MembersMustExist : Member 'public Avalonia.StyledProperty<System.Boolean> Avalonia.StyledProperty<System.Boolean> Avalonia.Controls.Primitives.Popup.StaysOpenProperty' does not exist in the implementation but it does exist in the contract.
MembersMustExist : Member 'public void Avalonia.Controls.Primitives.Popup.add_Closed(System.EventHandler<Avalonia.Controls.Primitives.PopupClosedEventArgs>)' does not exist in the implementation but it does exist in the contract.
MembersMustExist : Member 'public void Avalonia.Controls.Primitives.Popup.remove_Closed(System.EventHandler<Avalonia.Controls.Primitives.PopupClosedEventArgs>)' does not exist in the implementation but it does exist in the contract.
TypesMustExist : Type 'Avalonia.Controls.Primitives.PopupClosedEventArgs' does not exist in the implementation but it does exist in the contract.
Total Issues: 5

7
src/Avalonia.Controls/AutoCompleteBox.cs

@ -1647,7 +1647,7 @@ namespace Avalonia.Controls
/// </summary>
/// <param name="sender">The source object.</param>
/// <param name="e">The event data.</param>
private void DropDownPopup_Closed(object sender, PopupClosedEventArgs e)
private void DropDownPopup_Closed(object sender, EventArgs e)
{
// Force the drop down dependency property to be false.
if (IsDropDownOpen)
@ -1655,11 +1655,6 @@ namespace Avalonia.Controls
IsDropDownOpen = false;
}
if (e.CloseEvent is PointerEventArgs pointerEvent)
{
pointerEvent.Handled = true;
}
// Fire the DropDownClosed event
if (_popupHasOpened)
{

7
src/Avalonia.Controls/Calendar/CalendarDatePicker.cs

@ -889,17 +889,12 @@ namespace Avalonia.Controls
_ignoreButtonClick = false;
}
}
private void PopUp_Closed(object sender, PopupClosedEventArgs e)
private void PopUp_Closed(object sender, EventArgs e)
{
IsDropDownOpen = false;
if(!_isPopupClosing)
{
if (e.CloseEvent is PointerEventArgs pointerEvent)
{
pointerEvent.Handled = true;
}
_isPopupClosing = true;
Threading.Dispatcher.UIThread.InvokeAsync(() => _isPopupClosing = false);
}

22
src/Avalonia.Controls/ComboBox.cs

@ -290,24 +290,6 @@ namespace Avalonia.Controls
_popup = e.NameScope.Get<Popup>("PART_Popup");
_popup.Opened += PopupOpened;
_popup.Closed += PopupClosed;
}
/// <summary>
/// Called when the ComboBox popup is closed, with the <see cref="PopupClosedEventArgs"/>
/// that caused the popup to close.
/// </summary>
/// <param name="e">The event args.</param>
/// <remarks>
/// This method can be overridden to control whether the event that caused the popup to close
/// is swallowed or passed through.
/// </remarks>
protected virtual void PopupClosedOverride(PopupClosedEventArgs e)
{
if (e.CloseEvent is PointerEventArgs pointerEvent)
{
pointerEvent.Handled = true;
}
}
internal void ItemFocused(ComboBoxItem dropDownItem)
@ -318,13 +300,11 @@ namespace Avalonia.Controls
}
}
private void PopupClosed(object sender, PopupClosedEventArgs e)
private void PopupClosed(object sender, EventArgs e)
{
_subscriptionsOnOpen?.Dispose();
_subscriptionsOnOpen = null;
PopupClosedOverride(e);
if (CanFocus(this))
{
Focus();

3
src/Avalonia.Controls/ContextMenu.cs

@ -265,7 +265,8 @@ namespace Avalonia.Controls
PlacementMode = PlacementMode,
PlacementRect = PlacementRect,
PlacementTarget = PlacementTarget ?? control,
StaysOpen = false
IsLightDismissEnabled = true,
OverlayDismissEventPassThrough = true,
};
_popup.Opened += PopupOpened;

4
src/Avalonia.Controls/IMenuElement.cs

@ -1,6 +1,8 @@
using System.Collections.Generic;
using Avalonia.Input;
#nullable enable
namespace Avalonia.Controls
{
/// <summary>
@ -11,7 +13,7 @@ namespace Avalonia.Controls
/// <summary>
/// Gets or sets the currently selected submenu item.
/// </summary>
IMenuItem SelectedItem { get; set; }
IMenuItem? SelectedItem { get; set; }
/// <summary>
/// Gets the submenu items.

6
src/Avalonia.Controls/IMenuItem.cs

@ -1,4 +1,6 @@
namespace Avalonia.Controls
#nullable enable
namespace Avalonia.Controls
{
/// <summary>
/// Represents a <see cref="MenuItem"/>.
@ -29,7 +31,7 @@
/// <summary>
/// Gets the parent <see cref="IMenuElement"/>.
/// </summary>
new IMenuElement Parent { get; }
new IMenuElement? Parent { get; }
/// <summary>
/// Raises a click event on the menu item.

2
src/Avalonia.Controls/ItemsControl.cs

@ -295,7 +295,7 @@ namespace Avalonia.Controls
if (next != null)
{
focus.Focus(next, NavigationMethod.Directional);
focus.Focus(next, NavigationMethod.Directional, e.KeyModifiers);
e.Handled = true;
}

3
src/Avalonia.Controls/ListBox.cs

@ -136,7 +136,8 @@ namespace Avalonia.Controls
e.Handled = UpdateSelectionFromEventSource(
e.Source,
true,
(e.KeyModifiers & KeyModifiers.Shift) != 0);
(e.KeyModifiers & KeyModifiers.Shift) != 0,
(e.KeyModifiers & KeyModifiers.Control) != 0);
}
}

5
src/Avalonia.Controls/Menu.cs

@ -1,9 +1,12 @@
using Avalonia.Controls.Platform;
using Avalonia.Controls.Primitives;
using Avalonia.Controls.Templates;
using Avalonia.Input;
using Avalonia.Interactivity;
using Avalonia.Layout;
#nullable enable
namespace Avalonia.Controls
{
/// <summary>
@ -14,6 +17,8 @@ namespace Avalonia.Controls
private static readonly ITemplate<IPanel> DefaultPanel =
new FuncTemplate<IPanel>(() => new StackPanel { Orientation = Orientation.Horizontal });
private LightDismissOverlayLayer? _overlay;
/// <summary>
/// Initializes a new instance of the <see cref="Menu"/> class.
/// </summary>

8
src/Avalonia.Controls/MenuBase.cs

@ -8,6 +8,8 @@ using Avalonia.Input;
using Avalonia.Interactivity;
using Avalonia.LogicalTree;
#nullable enable
namespace Avalonia.Controls
{
/// <summary>
@ -51,9 +53,7 @@ namespace Avalonia.Controls
/// <param name="interactionHandler">The menu interaction handler.</param>
public MenuBase(IMenuInteractionHandler interactionHandler)
{
Contract.Requires<ArgumentNullException>(interactionHandler != null);
InteractionHandler = interactionHandler;
InteractionHandler = interactionHandler ?? throw new ArgumentNullException(nameof(interactionHandler));
}
/// <summary>
@ -77,7 +77,7 @@ namespace Avalonia.Controls
IMenuInteractionHandler IMenu.InteractionHandler => InteractionHandler;
/// <inheritdoc/>
IMenuItem IMenuElement.SelectedItem
IMenuItem? IMenuElement.SelectedItem
{
get
{

16
src/Avalonia.Controls/MenuItem.cs

@ -13,6 +13,8 @@ using Avalonia.Interactivity;
using Avalonia.LogicalTree;
using Avalonia.VisualTree;
#nullable enable
namespace Avalonia.Controls
{
/// <summary>
@ -23,7 +25,7 @@ namespace Avalonia.Controls
/// <summary>
/// Defines the <see cref="Command"/> property.
/// </summary>
public static readonly DirectProperty<MenuItem, ICommand> CommandProperty =
public static readonly DirectProperty<MenuItem, ICommand?> CommandProperty =
Button.CommandProperty.AddOwner<MenuItem>(
menuItem => menuItem.Command,
(menuItem, command) => menuItem.Command = command,
@ -95,7 +97,7 @@ namespace Avalonia.Controls
private static readonly ITemplate<IPanel> DefaultPanel =
new FuncTemplate<IPanel>(() => new StackPanel());
private ICommand _command;
private ICommand? _command;
private bool _commandCanExecute = true;
private Popup _popup;
@ -192,7 +194,7 @@ namespace Avalonia.Controls
/// <summary>
/// Gets or sets the command associated with the menu item.
/// </summary>
public ICommand Command
public ICommand? Command
{
get { return _command; }
set { SetAndRaise(CommandProperty, ref _command, value); }
@ -272,7 +274,7 @@ namespace Avalonia.Controls
bool IMenuItem.IsPointerOverSubMenu => _popup?.IsPointerOverPopup ?? false;
/// <inheritdoc/>
IMenuElement IMenuItem.Parent => Parent as IMenuElement;
IMenuElement? IMenuItem.Parent => Parent as IMenuElement;
protected override bool IsEnabledCore => base.IsEnabledCore && _commandCanExecute;
@ -280,7 +282,7 @@ namespace Avalonia.Controls
bool IMenuElement.MoveSelection(NavigationDirection direction, bool wrap) => MoveSelection(direction, wrap);
/// <inheritdoc/>
IMenuItem IMenuElement.SelectedItem
IMenuItem? IMenuElement.SelectedItem
{
get
{
@ -551,7 +553,7 @@ namespace Avalonia.Controls
/// <param name="e">The property change event.</param>
private void IsSelectedChanged(AvaloniaPropertyChangedEventArgs e)
{
if ((bool)e.NewValue)
if ((bool)e.NewValue!)
{
Focus();
}
@ -563,7 +565,7 @@ namespace Avalonia.Controls
/// <param name="e">The property change event.</param>
private void SubMenuOpenChanged(AvaloniaPropertyChangedEventArgs e)
{
var value = (bool)e.NewValue;
var value = (bool)e.NewValue!;
if (value)
{

12
src/Avalonia.Controls/Platform/DefaultMenuInteractionHandler.cs

@ -3,7 +3,6 @@ using Avalonia.Input;
using Avalonia.Input.Raw;
using Avalonia.Interactivity;
using Avalonia.LogicalTree;
using Avalonia.Platform;
using Avalonia.Rendering;
using Avalonia.Threading;
using Avalonia.VisualTree;
@ -235,7 +234,9 @@ namespace Avalonia.Controls.Platform
// If the the parent is an IMenu which successfully moved its selection,
// and the current menu is open then close the current menu and open the
// new menu.
if (item.IsSubMenuOpen && item.Parent is IMenu)
if (item.IsSubMenuOpen &&
item.Parent is IMenu &&
item.Parent.SelectedItem is object)
{
item.Close();
Open(item.Parent.SelectedItem, true);
@ -363,6 +364,11 @@ namespace Avalonia.Controls.Platform
}
else
{
if (item.IsTopLevel && item.Parent is IMainMenu mainMenu)
{
mainMenu.Open();
}
Open(item, false);
}
@ -385,7 +391,7 @@ namespace Avalonia.Controls.Platform
{
if (e.Source == Menu)
{
Menu.MoveSelection(NavigationDirection.First, true);
Menu?.MoveSelection(NavigationDirection.First, true);
}
}

61
src/Avalonia.Controls/Primitives/LightDismissOverlayLayer.cs

@ -0,0 +1,61 @@
using System;
using System.Linq;
using Avalonia.Controls.Templates;
using Avalonia.Input;
using Avalonia.Rendering;
using Avalonia.Styling;
using Avalonia.VisualTree;
#nullable enable
namespace Avalonia.Controls.Primitives
{
/// <summary>
/// A layer that is used to dismiss a <see cref="Popup"/> when the user clicks outside.
/// </summary>
public class LightDismissOverlayLayer : Border, ICustomHitTest
{
public IInputElement? InputPassThroughElement { get; set; }
/// <summary>
/// Returns the light dismiss overlay for a specified visual.
/// </summary>
/// <param name="visual">The visual.</param>
/// <returns>The light dismiss overlay, or null if none found.</returns>
public static LightDismissOverlayLayer? GetLightDismissOverlayLayer(IVisual visual)
{
visual = visual ?? throw new ArgumentNullException(nameof(visual));
VisualLayerManager? manager;
if (visual is TopLevel topLevel)
{
manager = topLevel.GetTemplateChildren()
.OfType<VisualLayerManager>()
.FirstOrDefault();
}
else
{
manager = visual.FindAncestorOfType<VisualLayerManager>();
}
return manager?.LightDismissOverlayLayer;
}
public bool HitTest(Point point)
{
if (InputPassThroughElement is object)
{
var p = point.Transform(this.TransformToVisual(VisualRoot)!.Value);
var hit = VisualRoot.GetVisualAt(p, x => x != this);
if (hit is object)
{
return !InputPassThroughElement.IsVisualAncestorOf(hit);
}
}
return true;
}
}
}

143
src/Avalonia.Controls/Primitives/Popup.cs

@ -1,12 +1,10 @@
using System;
using System.Diagnostics;
using System.Linq;
using System.Reactive.Disposables;
using Avalonia.Controls.Presenters;
using Avalonia.Controls.Primitives.PopupPositioning;
using Avalonia.Input;
using Avalonia.Input.Raw;
using Avalonia.Interactivity;
using Avalonia.LogicalTree;
using Avalonia.Metadata;
using Avalonia.Platform;
@ -86,12 +84,27 @@ namespace Avalonia.Controls.Primitives
AvaloniaProperty.Register<Popup, bool>(nameof(ObeyScreenEdges), true);
#pragma warning restore 618
public static readonly StyledProperty<bool> OverlayDismissEventPassThroughProperty =
AvaloniaProperty.Register<Popup, bool>(nameof(OverlayDismissEventPassThrough));
public static readonly DirectProperty<Popup, IInputElement> OverlayInputPassThroughElementProperty =
AvaloniaProperty.RegisterDirect<Popup, IInputElement>(
nameof(OverlayInputPassThroughElement),
o => o.OverlayInputPassThroughElement,
(o, v) => o.OverlayInputPassThroughElement = v);
/// <summary>
/// Defines the <see cref="HorizontalOffset"/> property.
/// </summary>
public static readonly StyledProperty<double> HorizontalOffsetProperty =
AvaloniaProperty.Register<Popup, double>(nameof(HorizontalOffset));
/// <summary>
/// Defines the <see cref="IsLightDismissEnabled"/> property.
/// </summary>
public static readonly StyledProperty<bool> IsLightDismissEnabledProperty =
AvaloniaProperty.Register<Popup, bool>(nameof(IsLightDismissEnabled));
/// <summary>
/// Defines the <see cref="VerticalOffset"/> property.
/// </summary>
@ -101,8 +114,13 @@ namespace Avalonia.Controls.Primitives
/// <summary>
/// Defines the <see cref="StaysOpen"/> property.
/// </summary>
public static readonly StyledProperty<bool> StaysOpenProperty =
AvaloniaProperty.Register<Popup, bool>(nameof(StaysOpen), true);
[Obsolete("Use IsLightDismissEnabledProperty")]
public static readonly DirectProperty<Popup, bool> StaysOpenProperty =
AvaloniaProperty.RegisterDirect<Popup, bool>(
nameof(StaysOpen),
o => o.StaysOpen,
(o, v) => o.StaysOpen = v,
true);
/// <summary>
/// Defines the <see cref="Topmost"/> property.
@ -113,6 +131,7 @@ namespace Avalonia.Controls.Primitives
private bool _isOpen;
private bool _ignoreIsOpenChanged;
private PopupOpenState? _openState;
private IInputElement _overlayInputPassThroughElement;
/// <summary>
/// Initializes static members of the <see cref="Popup"/> class.
@ -127,7 +146,7 @@ namespace Avalonia.Controls.Primitives
/// <summary>
/// Raised when the popup closes.
/// </summary>
public event EventHandler<PopupClosedEventArgs>? Closed;
public event EventHandler<EventArgs>? Closed;
/// <summary>
/// Raised when the popup opens.
@ -165,6 +184,18 @@ namespace Avalonia.Controls.Primitives
set;
}
/// <summary>
/// Gets or sets a value that determines how the <see cref="Popup"/> can be dismissed.
/// </summary>
/// <remarks>
/// Light dismiss is when the user taps on any area other than the popup.
/// </remarks>
public bool IsLightDismissEnabled
{
get => GetValue(IsLightDismissEnabledProperty);
set => SetValue(IsLightDismissEnabledProperty, value);
}
/// <summary>
/// Gets or sets a value indicating whether the popup is currently open.
/// </summary>
@ -246,6 +277,32 @@ namespace Avalonia.Controls.Primitives
set => SetValue(ObeyScreenEdgesProperty, value);
}
/// <summary>
/// Gets or sets a value indicating whether the event that closes the popup is passed
/// through to the parent window.
/// </summary>
/// <remarks>
/// When <see cref="IsLightDismissEnabled"/> is set to true, clicks outside the the popup
/// cause the popup to close. When <see cref="OverlayDismissEventPassThrough"/> is set to
/// false, these clicks will be handled by the popup and not be registered by the parent
/// window. When set to true, the events will be passed through to the parent window.
/// </remarks>
public bool OverlayDismissEventPassThrough
{
get => GetValue(OverlayDismissEventPassThroughProperty);
set => SetValue(OverlayDismissEventPassThroughProperty, value);
}
/// <summary>
/// Gets or sets an element that should receive pointer input events even when underneath
/// the popup's overlay.
/// </summary>
public IInputElement OverlayInputPassThroughElement
{
get => _overlayInputPassThroughElement;
set => SetAndRaise(OverlayInputPassThroughElementProperty, ref _overlayInputPassThroughElement, value);
}
/// <summary>
/// Gets or sets the Horizontal offset of the popup in relation to the <see cref="PlacementTarget"/>.
/// </summary>
@ -268,10 +325,11 @@ namespace Avalonia.Controls.Primitives
/// Gets or sets a value indicating whether the popup should stay open when the popup is
/// pressed or loses focus.
/// </summary>
[Obsolete("Use IsLightDismissEnabled")]
public bool StaysOpen
{
get { return GetValue(StaysOpenProperty); }
set { SetValue(StaysOpenProperty, value); }
get => !IsLightDismissEnabled;
set => IsLightDismissEnabled = !value;
}
/// <summary>
@ -363,14 +421,12 @@ namespace Avalonia.Controls.Primitives
if (parentPopupRoot?.Parent is Popup popup)
{
DeferCleanup(SubscribeToEventHandler<Popup, EventHandler<PopupClosedEventArgs>>(popup, ParentClosed,
DeferCleanup(SubscribeToEventHandler<Popup, EventHandler<EventArgs>>(popup, ParentClosed,
(x, handler) => x.Closed += handler,
(x, handler) => x.Closed -= handler));
}
}
DeferCleanup(topLevel.AddDisposableHandler(PointerPressedEvent, PointerPressedOutside, RoutingStrategies.Tunnel));
DeferCleanup(InputManager.Instance?.Process.Subscribe(ListenForNonClientClick));
var cleanupPopup = Disposable.Create((popupHost, handlerCleanup), state =>
@ -384,6 +440,29 @@ namespace Avalonia.Controls.Primitives
state.popupHost.Dispose();
});
if (IsLightDismissEnabled)
{
var dismissLayer = LightDismissOverlayLayer.GetLightDismissOverlayLayer(placementTarget);
if (dismissLayer != null)
{
dismissLayer.IsVisible = true;
dismissLayer.InputPassThroughElement = _overlayInputPassThroughElement;
DeferCleanup(Disposable.Create(() =>
{
dismissLayer.IsVisible = false;
dismissLayer.InputPassThroughElement = null;
}));
DeferCleanup(SubscribeToEventHandler<LightDismissOverlayLayer, EventHandler<PointerPressedEventArgs>>(
dismissLayer,
PointerPressedDismissOverlay,
(x, handler) => x.PointerPressed += handler,
(x, handler) => x.PointerPressed -= handler));
}
}
_openState = new PopupOpenState(topLevel, popupHost, cleanupPopup);
WindowManagerAddShadowHintChanged(popupHost, WindowManagerAddShadowHint);
@ -401,7 +480,7 @@ namespace Avalonia.Controls.Primitives
/// <summary>
/// Closes the popup.
/// </summary>
public void Close() => CloseCore(null);
public void Close() => CloseCore();
/// <summary>
/// Measures the control.
@ -471,7 +550,7 @@ namespace Avalonia.Controls.Primitives
}
}
private void CloseCore(EventArgs? closeEvent)
private void CloseCore()
{
if (_openState is null)
{
@ -491,24 +570,46 @@ namespace Avalonia.Controls.Primitives
IsOpen = false;
}
Closed?.Invoke(this, new PopupClosedEventArgs(closeEvent));
Closed?.Invoke(this, EventArgs.Empty);
}
private void ListenForNonClientClick(RawInputEventArgs e)
{
var mouse = e as RawPointerEventArgs;
if (!StaysOpen && mouse?.Type == RawPointerEventType.NonClientLeftButtonDown)
if (IsLightDismissEnabled && mouse?.Type == RawPointerEventType.NonClientLeftButtonDown)
{
CloseCore(e);
CloseCore();
}
}
private void PointerPressedOutside(object sender, PointerPressedEventArgs e)
private void PointerPressedDismissOverlay(object sender, PointerPressedEventArgs e)
{
if (!StaysOpen && e.Source is IVisual v && !IsChildOrThis(v))
if (IsLightDismissEnabled && e.Source is IVisual v && !IsChildOrThis(v))
{
CloseCore(e);
CloseCore();
if (OverlayDismissEventPassThrough)
{
PassThroughEvent(e);
}
}
}
private void PassThroughEvent(PointerPressedEventArgs e)
{
if (e.Source is LightDismissOverlayLayer layer &&
layer.GetVisualRoot() is IInputElement root)
{
var p = e.GetCurrentPoint(root);
var hit = root.InputHitTest(p.Position, x => x != layer);
if (hit != null)
{
e.Pointer.Capture(hit);
hit.RaiseEvent(e);
e.Handled = true;
}
}
}
@ -602,7 +703,7 @@ namespace Avalonia.Controls.Primitives
private void WindowDeactivated(object sender, EventArgs e)
{
if (!StaysOpen)
if (IsLightDismissEnabled)
{
Close();
}
@ -610,7 +711,7 @@ namespace Avalonia.Controls.Primitives
private void ParentClosed(object sender, EventArgs e)
{
if (!StaysOpen)
if (IsLightDismissEnabled)
{
Close();
}
@ -618,7 +719,7 @@ namespace Avalonia.Controls.Primitives
private void WindowLostFocus()
{
if(!StaysOpen)
if (IsLightDismissEnabled)
Close();
}

33
src/Avalonia.Controls/Primitives/PopupClosedEventArgs.cs

@ -1,33 +0,0 @@
using System;
using Avalonia.Interactivity;
#nullable enable
namespace Avalonia.Controls.Primitives
{
/// <summary>
/// Holds data for the <see cref="Popup.Closed"/> event.
/// </summary>
public class PopupClosedEventArgs : EventArgs
{
/// <summary>
/// Initializes a new instance of the <see cref="PopupClosedEventArgs"/> class.
/// </summary>
/// <param name="closeEvent"></param>
public PopupClosedEventArgs(EventArgs? closeEvent)
{
CloseEvent = closeEvent;
}
/// <summary>
/// Gets the event that closed the popup, if any.
/// </summary>
/// <remarks>
/// If <see cref="Popup.StaysOpen"/> is false, then this property will hold details of the
/// interaction that caused the popup to close if the close was caused by e.g. a pointer press
/// outside the popup. It can be used to mark the event as handled if the event should not
/// be propagated.
/// </remarks>
public EventArgs? CloseEvent { get; }
}
}

26
src/Avalonia.Controls/Primitives/VisualLayerManager.cs

@ -1,6 +1,6 @@
using System.Collections.Generic;
using Avalonia.LogicalTree;
using Avalonia.Metadata;
using Avalonia.Media;
namespace Avalonia.Controls.Primitives
{
@ -8,7 +8,8 @@ namespace Avalonia.Controls.Primitives
{
private const int AdornerZIndex = int.MaxValue - 100;
private const int ChromeZIndex = int.MaxValue - 99;
private const int OverlayZIndex = int.MaxValue - 98;
private const int LightDismissOverlayZIndex = int.MaxValue - 98;
private const int OverlayZIndex = int.MaxValue - 97;
private ILogicalRoot _logicalRoot;
private readonly List<Control> _layers = new List<Control>();
@ -62,6 +63,27 @@ namespace Avalonia.Controls.Primitives
}
}
public LightDismissOverlayLayer LightDismissOverlayLayer
{
get
{
if (IsPopup)
return null;
var rv = FindLayer<LightDismissOverlayLayer>();
if (rv == null)
{
rv = new LightDismissOverlayLayer
{
Background = Brushes.Transparent,
IsVisible = false
};
AddLayer(rv, LightDismissOverlayZIndex);
}
return rv;
}
}
T FindLayer<T>() where T : class
{
foreach (var layer in _layers)

37
src/Avalonia.Controls/Utils/SelectedItemsSync.cs

@ -1,6 +1,7 @@
using System;
using System.Collections;
using System.Collections.Specialized;
using System.ComponentModel;
using System.Linq;
using Avalonia.Collections;
@ -16,6 +17,7 @@ namespace Avalonia.Controls.Utils
private IList? _items;
private bool _updatingItems;
private bool _updatingModel;
private bool _initializeOnSourceAssignment;
public SelectedItemsSync(ISelectionModel model)
{
@ -63,10 +65,18 @@ namespace Avalonia.Controls.Utils
_updatingModel = true;
_items = items;
using (Model.Update())
if (Model.Source is object)
{
Model.ClearSelection();
Add(items);
using (Model.Update())
{
Model.ClearSelection();
Add(items);
}
}
else if (!_initializeOnSourceAssignment)
{
Model.PropertyChanged += SelectionModelPropertyChanged;
_initializeOnSourceAssignment = true;
}
if (_items is INotifyCollectionChanged incc2)
@ -86,9 +96,11 @@ namespace Avalonia.Controls.Utils
if (_items != null)
{
Model.PropertyChanged -= SelectionModelPropertyChanged;
Model.SelectionChanged -= SelectionModelSelectionChanged;
Model = model;
Model.SelectionChanged += SelectionModelSelectionChanged;
_initializeOnSourceAssignment = false;
try
{
@ -175,6 +187,25 @@ namespace Avalonia.Controls.Utils
}
}
private void SelectionModelPropertyChanged(object sender, PropertyChangedEventArgs e)
{
if (_initializeOnSourceAssignment &&
_items != null &&
e.PropertyName == nameof(SelectionModel.Source))
{
try
{
_updatingModel = true;
Add(_items);
_initializeOnSourceAssignment = false;
}
finally
{
_updatingModel = false;
}
}
}
private void SelectionModelSelectionChanged(object sender, SelectionModelSelectionChangedEventArgs e)
{
if (_updatingModel)

3
src/Avalonia.DesignerSupport/DesignWindowLoader.cs

@ -69,6 +69,8 @@ namespace Avalonia.DesignerSupport
window = new Window() {Content = (Control)control};
}
Design.ApplyDesignModeProperties(window, control);
if (!window.IsSet(Window.SizeToContentProperty))
{
if (double.IsNaN(window.Width))
@ -83,7 +85,6 @@ namespace Avalonia.DesignerSupport
}
}
window.Show();
Design.ApplyDesignModeProperties(window, control);
return window;
}
}

29
src/Avalonia.Input/InputExtensions.cs

@ -3,6 +3,8 @@ using System.Collections.Generic;
using System.Linq;
using Avalonia.VisualTree;
#nullable enable
namespace Avalonia.Input
{
/// <summary>
@ -22,7 +24,7 @@ namespace Avalonia.Input
/// </returns>
public static IEnumerable<IInputElement> GetInputElementsAt(this IInputElement element, Point p)
{
Contract.Requires<ArgumentNullException>(element != null);
element = element ?? throw new ArgumentNullException(nameof(element));
return element.GetVisualsAt(p, s_hitTestDelegate).Cast<IInputElement>();
}
@ -33,13 +35,34 @@ namespace Avalonia.Input
/// <param name="element">The element to test.</param>
/// <param name="p">The point on <paramref name="element"/>.</param>
/// <returns>The topmost <see cref="IInputElement"/> at the specified position.</returns>
public static IInputElement InputHitTest(this IInputElement element, Point p)
public static IInputElement? InputHitTest(this IInputElement element, Point p)
{
Contract.Requires<ArgumentNullException>(element != null);
element = element ?? throw new ArgumentNullException(nameof(element));
return element.GetVisualAt(p, s_hitTestDelegate) as IInputElement;
}
/// <summary>
/// Returns the topmost active input element at a point on an <see cref="IInputElement"/>.
/// </summary>
/// <param name="element">The element to test.</param>
/// <param name="p">The point on <paramref name="element"/>.</param>
/// <param name="filter">
/// A filter predicate. If the predicate returns false then the visual and all its
/// children will be excluded from the results.
/// </param>
/// <returns>The topmost <see cref="IInputElement"/> at the specified position.</returns>
public static IInputElement? InputHitTest(
this IInputElement element,
Point p,
Func<IVisual, bool> filter)
{
element = element ?? throw new ArgumentNullException(nameof(element));
filter = filter ?? throw new ArgumentNullException(nameof(filter));
return element.GetVisualAt(p, x => s_hitTestDelegate(x) && filter(x)) as IInputElement;
}
private static bool IsHitTestVisible(IVisual visual)
{
var element = visual as IInputElement;

3
src/Avalonia.Themes.Default/Accents/BaseDark.xaml

@ -58,6 +58,9 @@
<SolidColorBrush x:Key="NotificationCardWarningBackgroundBrush" Color="#FDB328" Opacity="0.75"/>
<SolidColorBrush x:Key="NotificationCardErrorBackgroundBrush" Color="#BD202C" Opacity="0.75"/>
<SolidColorBrush x:Key="DatePickerFlyoutPresenterHighlightFill" Color="{DynamicResource ThemeAccentColor}" Opacity="0.4" />
<SolidColorBrush x:Key="TimePickerFlyoutPresenterHighlightFill" Color="{DynamicResource ThemeAccentColor}" Opacity="0.4" />
<SolidColorBrush x:Key="ThemeControlTransparentBrush" Color="Transparent" />
<Thickness x:Key="ThemeBorderThickness">1,1,1,1</Thickness>

3
src/Avalonia.Themes.Default/Accents/BaseLight.xaml

@ -61,6 +61,9 @@
<SolidColorBrush x:Key="NotificationCardWarningBackgroundBrush" Color="#FDB328" Opacity="0.75"/>
<SolidColorBrush x:Key="NotificationCardErrorBackgroundBrush" Color="#BD202C" Opacity="0.75"/>
<SolidColorBrush x:Key="DatePickerFlyoutPresenterHighlightFill" Color="{DynamicResource ThemeAccentColor}" Opacity="0.4" />
<SolidColorBrush x:Key="TimePickerFlyoutPresenterHighlightFill" Color="{DynamicResource ThemeAccentColor}" Opacity="0.4" />
<SolidColorBrush x:Key="ThemeControlTransparentBrush" Color="Transparent" />
<Thickness x:Key="ThemeBorderThickness">1</Thickness>

2
src/Avalonia.Themes.Default/AutoCompleteBox.xaml

@ -19,7 +19,7 @@
MinWidth="{Binding Bounds.Width, RelativeSource={RelativeSource TemplatedParent}}"
MaxHeight="{TemplateBinding MaxDropDownHeight}"
PlacementTarget="{TemplateBinding}"
StaysOpen="False">
IsLightDismissEnabled="True">
<Border BorderBrush="{DynamicResource ThemeBorderMidBrush}"
BorderThickness="1">
<ListBox Name="PART_SelectingItemsControl"

2
src/Avalonia.Themes.Default/ComboBox.xaml

@ -66,7 +66,7 @@
MinWidth="{Binding Bounds.Width, RelativeSource={RelativeSource TemplatedParent}}"
MaxHeight="{TemplateBinding MaxDropDownHeight}"
PlacementTarget="{TemplateBinding}"
StaysOpen="False">
IsLightDismissEnabled="True">
<Border BorderBrush="{DynamicResource ThemeBorderMidBrush}"
BorderThickness="1">
<ScrollViewer>

334
src/Avalonia.Themes.Default/DatePicker.xaml

@ -0,0 +1,334 @@
<!--
// (c) Copyright Microsoft Corporation.
// This source is subject to the Microsoft Public License (Ms-PL).
// Please see http://go.microsoft.com/fwlink/?LinkID=131993 for details.
// All other rights reserved.
-->
<Styles xmlns="https://github.com/avaloniaui"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:sys="clr-namespace:System;assembly=netstandard">
<Styles.Resources>
<Thickness x:Key="DatePickerTopHeaderMargin">0,0,0,4</Thickness>
<x:Double x:Key="DatePickerFlyoutPresenterHighlightHeight">40</x:Double>
<x:Double x:Key="DatePickerFlyoutPresenterItemHeight">40</x:Double>
<x:Double x:Key="DatePickerFlyoutPresenterAcceptDismissHostGridHeight">41</x:Double>
<x:Double x:Key="DatePickerThemeMinWidth">296</x:Double>
<x:Double x:Key="DatePickerThemeMaxWidth">456</x:Double>
<Thickness x:Key="DatePickerFlyoutPresenterItemPadding">0,3,0,6</Thickness>
<Thickness x:Key="DatePickerFlyoutPresenterMonthPadding">9,3,0,6</Thickness>
<Thickness x:Key="DatePickerHostPadding">0,3,0,6</Thickness>
<Thickness x:Key="DatePickerHostMonthPadding">9,3,0,6</Thickness>
<x:Double x:Key="DatePickerSpacerThemeWidth">1</x:Double>
</Styles.Resources>
<!-- Styles for the items displayed in the selectors -->
<Style Selector="ListBoxItem.DateTimePickerItem">
<Setter Property="Padding" Value="{DynamicResource DatePickerFlyoutPresenterItemPadding}"/>
<Setter Property="VerticalContentAlignment" Value="Center" />
<Setter Property="HorizontalContentAlignment" Value="Center" />
</Style>
<Style Selector="ListBoxItem.DateTimePickerItem:selected">
<Setter Property="IsHitTestVisible" Value="False"/>
</Style>
<Style Selector="ListBoxItem.DateTimePickerItem:selected /template/ Rectangle#PressedBackground">
<Setter Property="Fill" Value="Transparent"/>
</Style>
<Style Selector="ListBoxItem.DateTimePickerItem:selected /template/ ContentPresenter">
<Setter Property="Background" Value="Transparent" />
<Setter Property="TextBlock.Foreground" Value="{DynamicResource ThemeForegroundBrush}"/>
</Style>
<Style Selector="ListBoxItem.DateTimePickerItem.MonthItem">
<Setter Property="Padding" Value="{DynamicResource DatePickerFlyoutPresenterMonthPadding}"/>
<Setter Property="VerticalContentAlignment" Value="Center" />
<Setter Property="HorizontalContentAlignment" Value="Left" />
</Style>
<!-- This is used for both the accept/dismiss & repeatbuttons in the presenter-->
<Style Selector=":is(Button).DateTimeFlyoutButtonStyle">
<Setter Property="Background" Value="{DynamicResource ThemeControlTransparentBrush}" />
<Setter Property="HorizontalContentAlignment" Value="Center"/>
<Setter Property="VerticalContentAlignment" Value="Center"/>
<Setter Property="Template">
<ControlTemplate>
<Border Background="{TemplateBinding Background}">
<ContentPresenter x:Name="ContentPresenter"
Background="{TemplateBinding Background}"
BorderBrush="{DynamicResource ThemeControlTransparentBrush}"
BorderThickness="{DynamicResource DateTimeFlyoutButtonBorderThickness}"
Content="{TemplateBinding Content}"
TextBlock.Foreground="{DynamicResource ThemeForegroundBrush}"
ContentTemplate="{TemplateBinding ContentTemplate}"
Padding="{TemplateBinding Padding}"
HorizontalContentAlignment="{TemplateBinding HorizontalContentAlignment}"
VerticalContentAlignment="{TemplateBinding VerticalContentAlignment}"/>
</Border>
</ControlTemplate>
</Setter>
</Style>
<Style Selector=":is(Button).DateTimeFlyoutButtonStyle:pointerover /template/ ContentPresenter">
<Setter Property="Background" Value="{DynamicResource ThemeControlHighlightLowBrush}"/>
<Setter Property="BorderBrush" Value="{DynamicResource ThemeControlTransparentBrush}"/>
<Setter Property="TextBlock.Foreground" Value="{DynamicResource ThemeForegroundBrush}"/>
</Style>
<Style Selector=":is(Button).DateTimeFlyoutButtonStyle:pressed /template/ ContentPresenter">
<Setter Property="Background" Value="{DynamicResource ThemeControlHighlightMidBrush}"/>
<Setter Property="BorderBrush" Value="{DynamicResource ThemeControlTransparentBrush}"/>
<Setter Property="TextBlock.Foreground" Value="{DynamicResource ThemeForegroundBrush}"/>
</Style>
<Style Selector="RepeatButton.UpButton">
<Setter Property="VerticalAlignment" Value="Top"/>
<Setter Property="Height" Value="22" />
<Setter Property="HorizontalAlignment" Value="Stretch" />
<Setter Property="Focusable" Value="False" />
<Setter Property="Background" Value="{DynamicResource ThemeControlHighlightLowBrush}" />
<Setter Property="Content">
<Template>
<Viewbox Height="10" Width="10" HorizontalAlignment="Center" VerticalAlignment="Center">
<Path Stroke="{Binding $parent[RepeatButton].Foreground}" StrokeThickness="1" Data="M 0,9 L 9,0 L 18,9"/>
</Viewbox>
</Template>
</Setter>
</Style>
<Style Selector="RepeatButton.DownButton">
<Setter Property="VerticalAlignment" Value="Bottom"/>
<Setter Property="Height" Value="22" />
<Setter Property="HorizontalAlignment" Value="Stretch" />
<Setter Property="Focusable" Value="False" />
<Setter Property="Background" Value="{DynamicResource ThemeControlHighlightLowBrush}" />
<Setter Property="Content">
<Template>
<Viewbox Height="10" Width="10" HorizontalAlignment="Center" VerticalAlignment="Center">
<Path Stroke="{Binding $parent[RepeatButton].Foreground}" StrokeThickness="1" Data="M 0,0 L 9,9 L 18,0"/>
</Viewbox>
</Template>
</Setter>
</Style>
<Style Selector="DatePicker">
<Setter Property="FontSize" Value="{DynamicResource FontSizeNormal}" />
<Setter Property="Foreground" Value="{DynamicResource ThemeForegroundBrush}" />
<Setter Property="Background" Value="{DynamicResource ThemeBackgroundBrush}"/>
<Setter Property="BorderBrush" Value="{DynamicResource ThemeControlHighlightMidBrush}"/>
<Setter Property="BorderThickness" Value="1"/>
<Setter Property="HorizontalAlignment" Value="Left" />
<Setter Property="VerticalAlignment" Value="Center" />
<Setter Property="Template">
<ControlTemplate>
<Grid Name="LayoutRoot" Margin="{TemplateBinding Padding}" RowDefinitions="Auto,*">
<ContentPresenter Name="HeaderContentPresenter" Grid.Row="0"
Content="{TemplateBinding Header}"
ContentTemplate="{TemplateBinding HeaderTemplate}"
Margin="{DynamicResource DatePickerTopHeaderMargin}"
MaxWidth="{DynamicResource DatePickerThemeMaxWidth}"
HorizontalAlignment="Stretch"
VerticalAlignment="Top"/>
<Button Name="FlyoutButton" Grid.Row="1"
Foreground="{TemplateBinding Foreground}"
Background="{TemplateBinding Background}"
BorderBrush="{TemplateBinding BorderBrush}"
BorderThickness="{TemplateBinding BorderThickness}"
IsEnabled="{TemplateBinding IsEnabled}"
MinWidth="{DynamicResource DatePickerThemeMinWidth}"
MaxWidth="{DynamicResource DatePickerThemeMaxWidth}"
HorizontalAlignment="Stretch"
VerticalAlignment="Stretch"
HorizontalContentAlignment="Stretch"
VerticalContentAlignment="Stretch"
TemplatedControl.IsTemplateFocusTarget="True">
<Button.Template>
<ControlTemplate>
<ContentPresenter Name="ContentPresenter"
BorderBrush="{TemplateBinding BorderBrush}"
Background="{TemplateBinding Background}"
BorderThickness="{TemplateBinding BorderThickness}"
Content="{TemplateBinding Content}"
TextBlock.Foreground="{TemplateBinding Foreground}"
HorizontalContentAlignment="Stretch"
VerticalContentAlignment="Stretch"/>
</ControlTemplate>
</Button.Template>
<Grid Name="ButtonContentGrid" ColumnDefinitions="78*,Auto,132*,Auto,78*">
<TextBlock Name="DayText" Text="day" HorizontalAlignment="Center"
Padding="{DynamicResource DatePickerHostPadding}"
FontFamily="{TemplateBinding FontFamily}"
FontWeight="{TemplateBinding FontWeight}"
FontSize="{TemplateBinding FontSize}"/>
<TextBlock Name="MonthText" Text="month" TextAlignment="Left"
Padding="{DynamicResource DatePickerHostMonthPadding}"
FontFamily="{TemplateBinding FontFamily}"
FontWeight="{TemplateBinding FontWeight}"
FontSize="{TemplateBinding FontSize}"/>
<TextBlock Name="YearText" Text="year" HorizontalAlignment="Center"
Padding="{DynamicResource DatePickerHostPadding}"
FontFamily="{TemplateBinding FontFamily}"
FontWeight="{TemplateBinding FontWeight}"
FontSize="{TemplateBinding FontSize}"/>
<Rectangle x:Name="FirstSpacer"
Fill="{DynamicResource ThemeControlMidHighBrush}"
HorizontalAlignment="Center"
Width="1"
Grid.Column="1" />
<Rectangle x:Name="SecondSpacer"
Fill="{DynamicResource ThemeControlMidHighBrush}"
HorizontalAlignment="Center"
Width="1"
Grid.Column="3" />
</Grid>
</Button>
<Popup Name="Popup" WindowManagerAddShadowHint="False"
StaysOpen="False" PlacementTarget="{TemplateBinding}"
PlacementMode="Bottom">
<DatePickerPresenter Name="PickerPresenter" />
</Popup>
</Grid>
</ControlTemplate>
</Setter>
</Style>
<Style Selector="DatePicker /template/ ContentPresenter#HeaderContentPresenter">
<Setter Property="TextBlock.Foreground" Value="{DynamicResource ThemeForegroundBrush}"/>
</Style>
<Style Selector="DatePicker:disabled /template/ Rectangle">
<!--<Setter Property="Fill" Value="{DynamicResource DatePickerSpacerFillDisabled}"/>-->
<Setter Property="Opacity" Value="{DynamicResource ThemeDisabledOpacity}" />
</Style>
<Style Selector="DatePicker /template/ Button#FlyoutButton:pointerover /template/ ContentPresenter">
<Setter Property="BorderBrush" Value="{DynamicResource ThemeControlHighBrush}"/>
<!--<Setter Property="Background" Value="{DynamicResource DatePickerButtonBackgroundPointerOver}"/>-->
<Setter Property="TextBlock.Foreground" Value="{DynamicResource ThemeForegroundBrush}"/>
</Style>
<Style Selector="DatePicker /template/ Button#FlyoutButton:pressed /template/ ContentPresenter">
<Setter Property="BorderBrush" Value="{DynamicResource ThemeControlLowBrush}"/>
<Setter Property="Background">
<SolidColorBrush Color="{DynamicResource ThemeControlMidHighColor}" Opacity="0.6" />
</Setter>
<Setter Property="TextBlock.Foreground" Value="{DynamicResource ThemeForegroundBrush}"/>
</Style>
<Style Selector="DatePicker /template/ Button#FlyoutButton:disabled /template/ ContentPresenter">
<!--<Setter Property="BorderBrush" Value="{DynamicResource DatePickerButtonBorderBrushDisabled}"/>
<Setter Property="Background" Value="{DynamicResource DatePickerButtonBackgroundDisabled}"/>
<Setter Property="TextBlock.Foreground" Value="{DynamicResource DatePickerButtonForegroundDisabled}"/>-->
<Setter Property="Opacity" Value="{DynamicResource ThemeDisabledOpacity}" />
</Style>
<!-- Changes foreground for watermark text when SelectedDate is null-->
<Style Selector="DatePicker:hasnodate /template/ Button#FlyoutButton TextBlock">
<Setter Property="Foreground" Value="{DynamicResource ThemeForegroundLowBrush}"/>
</Style>
<!--WinUI: DatePickerFlyoutPresenter-->
<Style Selector="DatePickerPresenter">
<Setter Property="Width" Value="296" />
<Setter Property="MinWidth" Value="296" />
<Setter Property="MaxHeight" Value="398" />
<Setter Property="FontWeight" Value="Normal" />
<Setter Property="FontSize" Value="{DynamicResource FontSizeNormal}" />
<Setter Property="Background" Value="{DynamicResource ThemeBackgroundBrush}" />
<Setter Property="BorderBrush" Value="{DynamicResource ThemeBorderMidBrush}" />
<Setter Property="BorderThickness" Value="{DynamicResource DateTimeFlyoutBorderThickness}" />
<Setter Property="Template">
<ControlTemplate>
<Border Name="Background" Background="{TemplateBinding Background}"
BorderBrush="{TemplateBinding BorderBrush}"
BorderThickness="{TemplateBinding BorderThickness}"
Padding="{DynamicResource DateTimeFlyoutBorderPadding}"
MaxHeight="398">
<Grid Name="ContentRoot" RowDefinitions="*,Auto">
<Grid Name="PickerContainer">
<!--Column Definitions set in code, ignore here-->
<Panel Name="MonthHost">
<ScrollViewer HorizontalScrollBarVisibility="Disabled"
VerticalScrollBarVisibility="Hidden">
<DateTimePickerPanel Name="MonthSelector"
PanelType="Month"
ItemHeight="{DynamicResource DatePickerFlyoutPresenterItemHeight}"
ShouldLoop="True" />
</ScrollViewer>
<RepeatButton Name="MonthUpButton"
Classes="DateTimeFlyoutButtonStyle UpButton"/>
<RepeatButton Name="MonthDownButton"
Classes="DateTimeFlyoutButtonStyle DownButton"/>
</Panel>
<Panel Name="DayHost">
<ScrollViewer HorizontalScrollBarVisibility="Disabled"
VerticalScrollBarVisibility="Hidden">
<DateTimePickerPanel Name="DaySelector"
PanelType="Day"
ItemHeight="{DynamicResource DatePickerFlyoutPresenterItemHeight}"
ShouldLoop="True" />
</ScrollViewer>
<RepeatButton Name="DayUpButton"
Classes="DateTimeFlyoutButtonStyle UpButton"/>
<RepeatButton Name="DayDownButton"
Classes="DateTimeFlyoutButtonStyle DownButton"/>
</Panel>
<Panel Name="YearHost">
<ScrollViewer HorizontalScrollBarVisibility="Disabled"
VerticalScrollBarVisibility="Hidden">
<DateTimePickerPanel Name="YearSelector"
PanelType="Year"
ItemHeight="{DynamicResource DatePickerFlyoutPresenterItemHeight}"
ShouldLoop="False" />
</ScrollViewer>
<RepeatButton Name="YearUpButton"
Classes="DateTimeFlyoutButtonStyle UpButton"/>
<RepeatButton Name="YearDownButton"
Classes="DateTimeFlyoutButtonStyle DownButton"/>
</Panel>
<Rectangle Name="HighlightRect" IsHitTestVisible="False" ZIndex="-1"
Fill="{DynamicResource DatePickerFlyoutPresenterHighlightFill}"
Grid.Column="0" Grid.ColumnSpan="5" VerticalAlignment="Center"
Height="{DynamicResource DatePickerFlyoutPresenterHighlightHeight}" />
<Rectangle Name="FirstSpacer"
Fill="{DynamicResource ThemeControlMidHighBrush}"
HorizontalAlignment="Center"
Width="{DynamicResource DatePickerSpacerThemeWidth}"
Grid.Column="1" />
<Rectangle Name="SecondSpacer"
Fill="{DynamicResource ThemeControlMidHighBrush}"
HorizontalAlignment="Center"
Width="{DynamicResource DatePickerSpacerThemeWidth}"
Grid.Column="3" />
</Grid>
<Grid Grid.Row="1" Height="{DynamicResource DatePickerFlyoutPresenterAcceptDismissHostGridHeight}"
Name="AcceptDismissGrid" ColumnDefinitions="*,*">
<Rectangle Height="{DynamicResource DatePickerSpacerThemeWidth}" VerticalAlignment="Top"
Fill="{DynamicResource ThemeControlMidHighBrush}"
Grid.ColumnSpan="2"/>
<Button Name="AcceptButton" Grid.Column="0" Classes="DateTimeFlyoutButtonStyle"
HorizontalAlignment="Stretch" VerticalAlignment="Stretch">
<Path Stroke="{Binding $parent[Button].Foreground}" StrokeLineCap="Round"
StrokeThickness="0.75" Data="M0.5,8.5 5,13.5 15.5,3" />
</Button>
<Button Name="DismissButton" Grid.Column="1" Classes="DateTimeFlyoutButtonStyle"
FontSize="16" HorizontalAlignment="Stretch" VerticalAlignment="Stretch">
<Path Stroke="{Binding $parent[Button].Foreground}" StrokeLineCap="Round"
StrokeThickness="0.75" Data="M2,2 14,14 M2,14 14 2" />
</Button>
</Grid>
</Grid>
</Border>
</ControlTemplate>
</Setter>
</Style>
<Style Selector="DatePickerPresenter /template/ Panel RepeatButton">
<Setter Property="IsVisible" Value="False" />
</Style>
<Style Selector="DatePickerPresenter /template/ Panel:pointerover RepeatButton">
<Setter Property="IsVisible" Value="True" />
</Style>
</Styles>

3
src/Avalonia.Themes.Default/DefaultTheme.xaml

@ -55,4 +55,7 @@
<StyleInclude Source="resm:Avalonia.Themes.Default.NotificationCard.xaml?assembly=Avalonia.Themes.Default"/>
<StyleInclude Source="resm:Avalonia.Themes.Default.NativeMenuBar.xaml?assembly=Avalonia.Themes.Default"/>
<StyleInclude Source="resm:Avalonia.Themes.Default.ToggleSwitch.xaml?assembly=Avalonia.Themes.Default"/>
<StyleInclude Source="resm:Avalonia.Themes.Default.SplitView.xaml?assembly=Avalonia.Themes.Default"/>
<StyleInclude Source="resm:Avalonia.Themes.Default.DatePicker.xaml?assembly=Avalonia.Themes.Default"/>
<StyleInclude Source="resm:Avalonia.Themes.Default.TimePicker.xaml?assembly=Avalonia.Themes.Default"/>
</Styles>

4
src/Avalonia.Themes.Default/GridSplitter.xaml

@ -2,8 +2,8 @@
<Style Selector="GridSplitter">
<Setter Property="Focusable" Value="True" />
<Setter Property="MinWidth" Value="6" />
<Setter Property="MinHeight" Value="6" />
<Setter Property="MinWidth" Value="1" />
<Setter Property="MinHeight" Value="1" />
<Setter Property="Background" Value="{DynamicResource ThemeControlMidBrush}" />
<Setter Property="PreviewContent">
<Template>

6
src/Avalonia.Themes.Default/MenuItem.xaml

@ -59,7 +59,7 @@
Grid.Column="4"/>
<Popup Name="PART_Popup"
PlacementMode="Right"
StaysOpen="True"
IsLightDismissEnabled="True"
IsOpen="{TemplateBinding IsSubMenuOpen, Mode=TwoWay}">
<Border Background="{TemplateBinding Background}"
BorderBrush="{DynamicResource ThemeBorderMidBrush}"
@ -108,8 +108,8 @@
</ContentPresenter.DataTemplates>
</ContentPresenter>
<Popup Name="PART_Popup"
IsOpen="{TemplateBinding IsSubMenuOpen, Mode=TwoWay}"
StaysOpen="True">
IsLightDismissEnabled="True"
IsOpen="{TemplateBinding IsSubMenuOpen, Mode=TwoWay}">
<Border Background="{TemplateBinding Background}"
BorderBrush="{DynamicResource ThemeBorderMidBrush}"
BorderThickness="{TemplateBinding BorderThickness}">

219
src/Avalonia.Themes.Default/SplitView.xaml

@ -0,0 +1,219 @@
<Styles xmlns="https://github.com/avaloniaui"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml">
<Styles.Resources>
<x:Double x:Key="SplitViewOpenPaneThemeLength">320</x:Double>
<x:Double x:Key="SplitViewCompactPaneThemeLength">48</x:Double>
<!-- Not used here (directly) since they're strings, but preserving for reference
<x:String x:Key="SplitViewPaneAnimationOpenDuration">00:00:00.2</x:String>
<x:String x:Key="SplitViewPaneAnimationOpenPreDuration">00:00:00.19999</x:String>
<x:String x:Key="SplitViewPaneAnimationCloseDuration">00:00:00.1</x:String>-->
</Styles.Resources>
<Style Selector="SplitView">
<Setter Property="OpenPaneLength" Value="{DynamicResource SplitViewOpenPaneThemeLength}" />
<Setter Property="CompactPaneLength" Value="{DynamicResource SplitViewCompactPaneThemeLength}" />
<Setter Property="PaneBackground" Value="{DynamicResource ThemeControlHighlightLowBrush}" />
</Style>
<!-- Left -->
<Style Selector="SplitView:left">
<Setter Property="Template">
<ControlTemplate>
<Grid Name="Container" Background="{TemplateBinding Background}">
<Grid.ColumnDefinitions>
<!-- why is this throwing a binding error? -->
<ColumnDefinition Width="{Binding RelativeSource={RelativeSource TemplatedParent}, Path=TemplateSettings.PaneColumnGridLength}"/>
<ColumnDefinition Width="*"/>
</Grid.ColumnDefinitions>
<Panel Name="PART_PaneRoot" Background="{TemplateBinding PaneBackground}"
ClipToBounds="True"
HorizontalAlignment="Left"
ZIndex="100">
<Border Child="{TemplateBinding Pane}"/>
<Rectangle Name="HCPaneBorder" Fill="{DynamicResource SystemControlForegroundTransparentBrush}" Width="1" HorizontalAlignment="Right" />
</Panel>
<Panel Name="ContentRoot">
<Border Child="{TemplateBinding Content}" />
<Rectangle Name="LightDismissLayer"/>
</Panel>
</Grid>
</ControlTemplate>
</Setter>
</Style>
<!-- Overlay -->
<Style Selector="SplitView:overlay:left /template/ Panel#PART_PaneRoot">
<Setter Property="Width" Value="{Binding RelativeSource={RelativeSource TemplatedParent}, Path=TemplateSettings.ClosedPaneWidth}" />
<!-- ColumnSpan should be 2 -->
<Setter Property="Grid.ColumnSpan" Value="1"/>
<Setter Property="Grid.Column" Value="0"/>
</Style>
<Style Selector="SplitView:overlay:left /template/ Panel#ContentRoot">
<Setter Property="Grid.Column" Value="1"/>
<Setter Property="Grid.ColumnSpan" Value="2"/>
</Style>
<!-- CompactInline -->
<Style Selector="SplitView:compactinline:left /template/ Panel#PART_PaneRoot">
<Setter Property="Grid.ColumnSpan" Value="1"/>
<Setter Property="Grid.Column" Value="0"/>
<Setter Property="Width" Value="{Binding RelativeSource={RelativeSource TemplatedParent}, Path=TemplateSettings.ClosedPaneWidth}" />
</Style>
<Style Selector="SplitView:compactinline:left /template/ Panel#ContentRoot">
<Setter Property="Grid.Column" Value="1"/>
<Setter Property="Grid.ColumnSpan" Value="1"/>
</Style>
<!-- CompactOverlay -->
<Style Selector="SplitView:compactoverlay:left /template/ Panel#PART_PaneRoot">
<!-- ColumnSpan should be 2 -->
<Setter Property="Grid.ColumnSpan" Value="1"/>
<Setter Property="Grid.Column" Value="0"/>
<Setter Property="Width" Value="{Binding RelativeSource={RelativeSource TemplatedParent}, Path=TemplateSettings.ClosedPaneWidth}" />
</Style>
<Style Selector="SplitView:compactoverlay:left /template/ Panel#ContentRoot">
<Setter Property="Grid.Column" Value="1"/>
<Setter Property="Grid.ColumnSpan" Value="1"/>
</Style>
<!-- Inline -->
<Style Selector="SplitView:inline:left /template/ Panel#PART_PaneRoot">
<Setter Property="Grid.ColumnSpan" Value="1"/>
<Setter Property="Grid.Column" Value="0"/>
<Setter Property="Width" Value="{Binding RelativeSource={RelativeSource TemplatedParent}, Path=TemplateSettings.ClosedPaneWidth}" />
</Style>
<Style Selector="SplitView:inline:left /template/ Panel#ContentRoot">
<Setter Property="Grid.Column" Value="1"/>
<Setter Property="Grid.ColumnSpan" Value="1"/>
</Style>
<!-- Right -->
<Style Selector="SplitView:right">
<Setter Property="Template">
<ControlTemplate>
<Grid Name="Container" Background="{TemplateBinding Background}">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="*"/>
<ColumnDefinition Width="{Binding RelativeSource={RelativeSource TemplatedParent}, Path=TemplateSettings.PaneColumnGridLength}"/>
</Grid.ColumnDefinitions>
<Panel Name="PART_PaneRoot" Background="{TemplateBinding PaneBackground}"
ClipToBounds="True"
HorizontalAlignment="Right"
ZIndex="100">
<Border Child="{TemplateBinding Pane}"/>
<Rectangle Name="HCPaneBorder"
Fill="{DynamicResource SystemControlForegroundTransparentBrush}"
Width="1" HorizontalAlignment="Left" />
</Panel>
<Panel Name="ContentRoot">
<Border Child="{TemplateBinding Content}" />
<Rectangle Name="LightDismissLayer"/>
</Panel>
</Grid>
</ControlTemplate>
</Setter>
</Style>
<!-- Overlay -->
<Style Selector="SplitView:overlay:right /template/ Panel#PART_PaneRoot">
<Setter Property="Width" Value="{Binding RelativeSource={RelativeSource TemplatedParent}, Path=TemplateSettings.ClosedPaneWidth}" />
<Setter Property="Grid.ColumnSpan" Value="2"/>
<Setter Property="Grid.Column" Value="1"/>
</Style>
<Style Selector="SplitView:overlay:right /template/ Panel#ContentRoot">
<Setter Property="Grid.Column" Value="0"/>
<Setter Property="Grid.ColumnSpan" Value="2"/>
</Style>
<!-- CompactInline -->
<Style Selector="SplitView:compactinline:right /template/ Panel#PART_PaneRoot">
<Setter Property="Grid.ColumnSpan" Value="1"/>
<Setter Property="Grid.Column" Value="1"/>
<Setter Property="Width" Value="{Binding RelativeSource={RelativeSource TemplatedParent}, Path=TemplateSettings.ClosedPaneWidth}" />
</Style>
<Style Selector="SplitView:compactinline:right /template/ Panel#ContentRoot">
<Setter Property="Grid.Column" Value="0"/>
<Setter Property="Grid.ColumnSpan" Value="1"/>
</Style>
<!-- CompactOverlay -->
<Style Selector="SplitView:compactoverlay:right /template/ Panel#PART_PaneRoot">
<Setter Property="Grid.ColumnSpan" Value="2"/>
<Setter Property="Grid.Column" Value="1"/>
<Setter Property="Width" Value="{Binding RelativeSource={RelativeSource TemplatedParent}, Path=TemplateSettings.ClosedPaneWidth}" />
</Style>
<Style Selector="SplitView:compactoverlay:right /template/ Panel#ContentRoot">
<Setter Property="Grid.Column" Value="0"/>
<Setter Property="Grid.ColumnSpan" Value="1"/>
</Style>
<!-- Inline -->
<Style Selector="SplitView:inline:right /template/ Panel#PART_PaneRoot">
<Setter Property="Grid.ColumnSpan" Value="1"/>
<Setter Property="Grid.Column" Value="1"/>
<Setter Property="Width" Value="{Binding RelativeSource={RelativeSource TemplatedParent}, Path=TemplateSettings.ClosedPaneWidth}" />
</Style>
<Style Selector="SplitView:inline:right /template/ Panel#ContentRoot">
<Setter Property="Grid.Column" Value="0"/>
<Setter Property="Grid.ColumnSpan" Value="1"/>
</Style>
<!-- Open/Close Pane animation -->
<Style Selector="SplitView:open /template/ Panel#PART_PaneRoot">
<Setter Property="Transitions">
<Transitions>
<DoubleTransition Property="Width" Duration="00:00:00.2" Easing="0.1,0.9,0.2,1.0" />
</Transitions>
</Setter>
<Setter Property="Width" Value="{Binding RelativeSource={RelativeSource TemplatedParent}, Path=OpenPaneLength}" />
</Style>
<Style Selector="SplitView:open /template/ Rectangle#LightDismissLayer">
<Setter Property="Transitions">
<Transitions>
<DoubleTransition Property="Opacity" Duration="00:00:00.2" Easing="0.1,0.9,0.2,1.0" />
</Transitions>
</Setter>
<Setter Property="Opacity" Value="1.0"/>
</Style>
<Style Selector="SplitView:closed /template/ Panel#PART_PaneRoot">
<Setter Property="Transitions">
<Transitions>
<DoubleTransition Property="Width" Duration="00:00:00.1" Easing="0.1,0.9,0.2,1.0" />
</Transitions>
</Setter>
<Setter Property="Width" Value="{Binding RelativeSource={RelativeSource TemplatedParent}, Path=TemplateSettings.ClosedPaneWidth}" />
</Style>
<Style Selector="SplitView:closed /template/ Rectangle#LightDismissLayer">
<Setter Property="Transitions">
<Transitions>
<DoubleTransition Property="Opacity" Duration="00:00:00.2" Easing="0.1,0.9,0.2,1.0" />
</Transitions>
</Setter>
<Setter Property="Opacity" Value="0.0"/>
</Style>
<Style Selector="SplitView /template/ Rectangle#LightDismissLayer">
<Setter Property="IsVisible" Value="False"/>
<Setter Property="Fill" Value="Transparent" />
</Style>
<Style Selector="SplitView:lightdismiss /template/ Rectangle#LightDismissLayer">
<Setter Property="Fill" Value="{DynamicResource SplitViewLightDismissOverlayBackground}" />
</Style>
<Style Selector="SplitView:overlay:open /template/ Rectangle#LightDismissLayer">
<Setter Property="IsVisible" Value="True"/>
</Style>
<Style Selector="SplitView:compactoverlay:open /template/ Rectangle#LightDismissLayer">
<Setter Property="IsVisible" Value="True"/>
</Style>
</Styles>

283
src/Avalonia.Themes.Default/TimePicker.xaml

@ -0,0 +1,283 @@
<!--
// (c) Copyright Microsoft Corporation.
// This source is subject to the Microsoft Public License (Ms-PL).
// Please see http://go.microsoft.com/fwlink/?LinkID=131993 for details.
// All other rights reserved.
-->
<Styles xmlns="https://github.com/avaloniaui"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:sys="clr-namespace:System;assembly=netstandard">
<Styles.Resources>
<x:Double x:Key="TimePickerFlyoutPresenterItemHeight">40</x:Double>
<x:Double x:Key="TimePickerSpacerThemeWidth">1</x:Double>
<Thickness x:Key="TimePickerBorderThemeThickness">1</Thickness>
<Thickness x:Key="TimePickerTopHeaderMargin">0,0,0,4</Thickness>
<x:Double x:Key="TimePickerFlyoutPresenterHighlightHeight">40</x:Double>
<x:Double x:Key="TimePickerFlyoutPresenterAcceptDismissHostGridHeight">41</x:Double>
<x:Double x:Key="TimePickerThemeMinWidth">242</x:Double>
<x:Double x:Key="TimePickerThemeMaxWidth">456</x:Double>
<Thickness x:Key="TimePickerFlyoutPresenterItemPadding">0,3,0,6</Thickness>
<Thickness x:Key="TimePickerHostPadding">0,3,0,6</Thickness>
</Styles.Resources>
<Style Selector="ListBoxItem.DateTimePickerItem.HourItem">
<Setter Property="Padding" Value="{DynamicResource TimePickerFlyoutPresenterItemPadding}" />
</Style>
<Style Selector="ListBoxItem.DateTimePickerItem.MinuteItem">
<Setter Property="Padding" Value="{DynamicResource TimePickerFlyoutPresenterItemPadding}" />
</Style>
<Style Selector="ListBoxItem.DateTimePickerItem.TimePeriodItem">
<Setter Property="Padding" Value="{DynamicResource TimePickerFlyoutPresenterItemPadding}" />
</Style>
<Style Selector="TimePicker">
<Setter Property="FontSize" Value="{DynamicResource FontSizeNormal}" />
<Setter Property="Foreground" Value="{DynamicResource ThemeForegroundBrush}" />
<Setter Property="Background" Value="{DynamicResource ThemeBackgroundBrush}"/>
<Setter Property="BorderBrush" Value="{DynamicResource ThemeControlHighlightMidBrush}"/>
<Setter Property="BorderThickness" Value="{DynamicResource TimePickerBorderThemeThickness}"/>
<Setter Property="HorizontalAlignment" Value="Left" />
<Setter Property="VerticalAlignment" Value="Center" />
<Setter Property="Template">
<ControlTemplate>
<Grid Name="LayoutRoot" Margin="{TemplateBinding Padding}" RowDefinitions="Auto,*">
<ContentPresenter x:Name="HeaderContentPresenter"
Grid.Row="0"
Content="{TemplateBinding Header}"
ContentTemplate="{TemplateBinding HeaderTemplate}"
Margin="{DynamicResource TimePickerTopHeaderMargin}"
MaxWidth="{DynamicResource TimePickerThemeMaxWidth}"
TextBlock.Foreground="{DynamicResource TimePickerHeaderForeground}"
HorizontalAlignment="Stretch"
VerticalAlignment="Top" />
<Button x:Name="FlyoutButton"
Grid.Row="1"
Foreground="{TemplateBinding Foreground}"
Background="{TemplateBinding Background}"
BorderBrush="{TemplateBinding BorderBrush}"
BorderThickness="{TemplateBinding BorderThickness}"
IsEnabled="{TemplateBinding IsEnabled}"
MinWidth="{DynamicResource TimePickerThemeMinWidth}"
MaxWidth="{DynamicResource TimePickerThemeMaxWidth}"
HorizontalAlignment="Stretch"
HorizontalContentAlignment="Stretch"
VerticalAlignment="Top"
VerticalContentAlignment="Stretch">
<Button.Template>
<ControlTemplate>
<ContentPresenter Name="ContentPresenter"
BorderBrush="{TemplateBinding BorderBrush}"
Background="{TemplateBinding Background}"
BorderThickness="{TemplateBinding BorderThickness}"
Content="{TemplateBinding Content}"
TextBlock.Foreground="{TemplateBinding Foreground}"
HorizontalContentAlignment="Stretch"
VerticalContentAlignment="Stretch" />
</ControlTemplate>
</Button.Template>
<Grid Name="FlyoutButtonContentGrid">
<!--Ignore col defs here, set in code-->
<Border x:Name="FirstPickerHost" Grid.Column="0" HorizontalAlignment="Stretch" VerticalAlignment="Stretch">
<TextBlock x:Name="HourTextBlock"
HorizontalAlignment="Center"
Padding="{DynamicResource TimePickerHostPadding}"
FontFamily="{TemplateBinding FontFamily}"
FontWeight="{TemplateBinding FontWeight}"
FontSize="{TemplateBinding FontSize}" />
</Border>
<Rectangle Name="FirstColumnDivider"
Fill="{DynamicResource ThemeControlMidHighBrush}"
HorizontalAlignment="Center"
Width="{DynamicResource TimePickerSpacerThemeWidth}"
Grid.Column="1" />
<Border x:Name="SecondPickerHost" Grid.Column="2" HorizontalAlignment="Stretch" VerticalAlignment="Stretch">
<TextBlock x:Name="MinuteTextBlock"
HorizontalAlignment="Center"
Padding="{DynamicResource TimePickerHostPadding}"
FontFamily="{TemplateBinding FontFamily}"
FontWeight="{TemplateBinding FontWeight}"
FontSize="{TemplateBinding FontSize}"/>
</Border>
<Rectangle Name="SecondColumnDivider"
Fill="{DynamicResource ThemeControlMidHighBrush}"
HorizontalAlignment="Center"
Width="{DynamicResource TimePickerSpacerThemeWidth}"
Grid.Column="3" />
<Border x:Name="ThirdPickerHost" Grid.Column="4" HorizontalAlignment="Stretch" VerticalAlignment="Stretch">
<TextBlock x:Name="PeriodTextBlock"
HorizontalAlignment="Center"
Padding="{DynamicResource TimePickerHostPadding}"
FontFamily="{TemplateBinding FontFamily}"
FontWeight="{TemplateBinding FontWeight}"
FontSize="{TemplateBinding FontSize}" />
</Border>
</Grid>
</Button>
<Popup Name="Popup" WindowManagerAddShadowHint="False"
StaysOpen="False" PlacementTarget="{TemplateBinding}"
PlacementMode="Bottom">
<TimePickerPresenter Name="PickerPresenter" />
</Popup>
</Grid>
</ControlTemplate>
</Setter>
</Style>
<Style Selector="TimePicker:disabled /template/ ContentPresenter#HeaderContentPresenter">
<Setter Property="TextBlock.Foreground" Value="{DynamicResource ThemeForegroundBrush}"/>
</Style>
<Style Selector="TimePicker:disabled /template/ Rectangle">
<Setter Property="Opacity" Value="{DynamicResource ThemeDisabledOpacity}"/>
</Style>
<Style Selector="TimePicker /template/ Button#FlyoutButton:pointerover /template/ ContentPresenter">
<!--<Setter Property="Background" Value="{DynamicResource TimePickerButtonBackgroundPointerOver}"/>-->
<Setter Property="BorderBrush" Value="{DynamicResource ThemeControlHighBrush}"/>
<Setter Property="TextBlock.Foreground" Value="{DynamicResource ThemeForegroundBrush}"/>
</Style>
<Style Selector="TimePicker /template/ Button:pressed /template/ ContentPresenter">
<Setter Property="Background">
<SolidColorBrush Color="{DynamicResource ThemeControlMidHighColor}" Opacity="0.6" />
</Setter>
<Setter Property="BorderBrush" Value="{DynamicResource ThemeControlLowBrush}"/>
<Setter Property="TextBlock.Foreground" Value="{DynamicResource ThemeForegroundBrush}"/>
</Style>
<Style Selector="TimePicker /template/ Button:disabled /template/ ContentPresenter">
<!--<Setter Property="Background" Value="{DynamicResource TimePickerButtonBackgroundDisabled}"/>
<Setter Property="BorderBrush" Value="{DynamicResource TimePickerButtonBorderBrushDisabled}"/>
<Setter Property="TextBlock.Foreground" Value="{DynamicResource TimePickerButtonForegroundDisabled}"/>-->
<Setter Property="Opacity" Value="{DynamicResource ThemeDisabledOpacity}" />
</Style>
<Style Selector="TimePicker:hasnotime /template/ Button#FlyoutButton TextBlock">
<Setter Property="Foreground" Value="{DynamicResource ThemeForegroundLowBrush}"/>
</Style>
<Style Selector="TimePickerPresenter">
<Setter Property="Width" Value="242" />
<Setter Property="MinWidth" Value="242" />
<Setter Property="MaxHeight" Value="398" />
<Setter Property="FontWeight" Value="Normal" />
<Setter Property="Background" Value="{DynamicResource ThemeBackgroundBrush}" />
<Setter Property="BorderBrush" Value="{DynamicResource ThemeBorderMidBrush}" />
<Setter Property="BorderThickness" Value="{DynamicResource DateTimeFlyoutBorderThickness}" />
<Setter Property="Template">
<ControlTemplate>
<Border Name="Background"
Background="{TemplateBinding Background}"
BorderBrush="{TemplateBinding BorderBrush}"
BorderThickness="{TemplateBinding BorderThickness}"
Padding="{DynamicResource DateTimeFlyoutBorderPadding}"
MaxHeight="398">
<Grid Name="ContentPanel" RowDefinitions="*,Auto">
<Grid Name="PickerContainer">
<!--Ignore col defs here, set in code-->
<Panel Name="HourHost" Grid.Column="0">
<ScrollViewer HorizontalScrollBarVisibility="Disabled"
VerticalScrollBarVisibility="Hidden">
<DateTimePickerPanel Name="HourSelector"
PanelType="Hour"
ShouldLoop="True"
ItemHeight="{DynamicResource TimePickerFlyoutPresenterItemHeight}"/>
</ScrollViewer>
<RepeatButton Name="HourUpButton"
Classes="DateTimeFlyoutButtonStyle UpButton"/>
<RepeatButton Name="HourDownButton"
Classes="DateTimeFlyoutButtonStyle DownButton"/>
</Panel>
<Panel Name="MinuteHost" Grid.Column="2">
<ScrollViewer HorizontalScrollBarVisibility="Disabled"
VerticalScrollBarVisibility="Hidden">
<DateTimePickerPanel Name="MinuteSelector"
PanelType="Minute"
ShouldLoop="True"
ItemHeight="{DynamicResource TimePickerFlyoutPresenterItemHeight}"/>
</ScrollViewer>
<RepeatButton Name="MinuteUpButton"
Classes="DateTimeFlyoutButtonStyle UpButton"/>
<RepeatButton Name="MinuteDownButton"
Classes="DateTimeFlyoutButtonStyle DownButton"/>
</Panel>
<Panel Name="PeriodHost" Grid.Column="4">
<ScrollViewer HorizontalScrollBarVisibility="Disabled"
VerticalScrollBarVisibility="Hidden">
<DateTimePickerPanel Name="PeriodSelector"
PanelType="TimePeriod"
ShouldLoop="False"
ItemHeight="{DynamicResource TimePickerFlyoutPresenterItemHeight}"/>
</ScrollViewer>
<RepeatButton Name="PeriodUpButton"
Classes="DateTimeFlyoutButtonStyle UpButton"/>
<RepeatButton Name="PeriodDownButton"
Classes="DateTimeFlyoutButtonStyle DownButton"/>
</Panel>
<Rectangle x:Name="HighlightRect" ZIndex="-1"
Fill="{DynamicResource TimePickerFlyoutPresenterHighlightFill}"
Grid.Column="0"
Grid.ColumnSpan="5"
VerticalAlignment="Center"
Height="{DynamicResource TimePickerFlyoutPresenterHighlightHeight}" />
<Rectangle Name="FirstSpacer"
Fill="{DynamicResource ThemeControlMidHighBrush}"
HorizontalAlignment="Center"
Width="{DynamicResource TimePickerSpacerThemeWidth}"
Grid.Column="1" />
<Rectangle Name="SecondSpacer"
Fill="{DynamicResource ThemeControlMidHighBrush}"
HorizontalAlignment="Center"
Width="{DynamicResource TimePickerSpacerThemeWidth}"
Grid.Column="3" />
</Grid>
<Grid Grid.Row="1" Height="{DynamicResource TimePickerFlyoutPresenterAcceptDismissHostGridHeight}"
Name="AcceptDismissHostGrid" ColumnDefinitions="*,*">
<Rectangle Height="{DynamicResource TimePickerSpacerThemeWidth}"
VerticalAlignment="Top"
Fill="{DynamicResource ThemeControlMidHighBrush}"
Grid.ColumnSpan="2" />
<Button Name="AcceptButton" Grid.Column="0" Classes="DateTimeFlyoutButtonStyle"
HorizontalAlignment="Stretch" VerticalAlignment="Stretch">
<Path Stroke="{Binding $parent[Button].Foreground}" StrokeLineCap="Round"
StrokeThickness="0.75" Data="M0.5,8.5 5,13.5 15.5,3" />
</Button>
<Button Name="DismissButton" Grid.Column="1" Classes="DateTimeFlyoutButtonStyle"
FontSize="16" HorizontalAlignment="Stretch" VerticalAlignment="Stretch">
<Path Stroke="{Binding $parent[Button].Foreground}" StrokeLineCap="Round"
StrokeThickness="0.75" Data="M2,2 14,14 M2,14 14 2" />
</Button>
</Grid>
</Grid>
</Border>
</ControlTemplate>
</Setter>
</Style>
<Style Selector="TimePickerPresenter /template/ Panel RepeatButton">
<Setter Property="IsVisible" Value="False" />
</Style>
<Style Selector="TimePickerPresenter /template/ Panel:pointerover RepeatButton">
<Setter Property="IsVisible" Value="True" />
</Style>
</Styles>

2
src/Avalonia.Themes.Fluent/AutoCompleteBox.xaml

@ -47,7 +47,7 @@
WindowManagerAddShadowHint="False"
MinWidth="{Binding Bounds.Width, RelativeSource={RelativeSource TemplatedParent}}"
MaxHeight="{TemplateBinding MaxDropDownHeight}"
StaysOpen="False"
IsLightDismissEnabled="True"
PlacementTarget="{TemplateBinding}">
<Border Name="PART_SuggestionsContainer"
Padding="{DynamicResource AutoCompleteListMargin}"

2
src/Avalonia.Themes.Fluent/CalendarDatePicker.xaml

@ -120,7 +120,7 @@
<Popup Name="PART_Popup"
PlacementTarget="{TemplateBinding}"
StaysOpen="False">
IsLightDismissEnabled="True">
<Calendar Name="PART_Calendar"
FirstDayOfWeek="{TemplateBinding FirstDayOfWeek}"
IsTodayHighlighted="{TemplateBinding IsTodayHighlighted}"/>

2
src/Avalonia.Themes.Fluent/ComboBox.xaml

@ -124,7 +124,7 @@
MinWidth="{Binding Bounds.Width, RelativeSource={RelativeSource TemplatedParent}}"
MaxHeight="{TemplateBinding MaxDropDownHeight}"
PlacementTarget="{TemplateBinding}"
StaysOpen="False">
IsLightDismissEnabled="True">
<Border x:Name="PopupBorder"
Background="{DynamicResource ComboBoxDropDownBackground}"
BorderBrush="{DynamicResource ComboBoxDropDownBorderBrush}"

2
src/Avalonia.Themes.Fluent/DatePicker.xaml

@ -190,7 +190,7 @@
</Button>
<Popup Name="Popup" WindowManagerAddShadowHint="False"
StaysOpen="False" PlacementTarget="{TemplateBinding}"
IsLightDismissEnabled="True" PlacementTarget="{TemplateBinding}"
PlacementMode="Bottom">
<DatePickerPresenter Name="PickerPresenter" />
</Popup>

5
src/Avalonia.Themes.Fluent/MenuItem.xaml

@ -112,7 +112,7 @@
WindowManagerAddShadowHint="True"
PlacementMode="Right"
HorizontalOffset="{StaticResource MenuFlyoutSubItemPopupHorizontalOffset}"
StaysOpen="True"
IsLightDismissEnabled="True"
IsOpen="{TemplateBinding IsSubMenuOpen, Mode=TwoWay}">
<Border Background="{DynamicResource MenuFlyoutPresenterBackground}"
BorderBrush="{DynamicResource MenuFlyoutPresenterBorderBrush}"
@ -159,8 +159,9 @@
<Popup Name="PART_Popup"
WindowManagerAddShadowHint="False"
MinWidth="{Binding Bounds.Width, RelativeSource={RelativeSource TemplatedParent}}"
IsLightDismissEnabled="True"
IsOpen="{TemplateBinding IsSubMenuOpen, Mode=TwoWay}"
StaysOpen="True">
OverlayInputPassThroughElement="{Binding $parent[Menu]}">
<Border Background="{DynamicResource MenuFlyoutPresenterBackground}"
BorderBrush="{DynamicResource MenuFlyoutPresenterBorderBrush}"
BorderThickness="{DynamicResource MenuFlyoutPresenterBorderThemeThickness}"

2
src/Avalonia.Themes.Fluent/TimePicker.xaml

@ -123,7 +123,7 @@
</Button>
<Popup Name="Popup" WindowManagerAddShadowHint="False"
StaysOpen="False" PlacementTarget="{TemplateBinding}"
IsLightDismissEnabled="True" PlacementTarget="{TemplateBinding}"
PlacementMode="Bottom">
<TimePickerPresenter Name="PickerPresenter" />
</Popup>

79
src/Avalonia.Visuals/Media/TextFormatting/TextFormatterImpl.cs

@ -31,7 +31,8 @@ namespace Avalonia.Media.TextFormatting
case TextWrapping.WrapWithOverflow:
case TextWrapping.Wrap:
{
textLine = PerformTextWrapping(textRuns, textRange, paragraphWidth, paragraphProperties);
textLine = PerformTextWrapping(textRuns, textRange, paragraphWidth, paragraphProperties,
nextLineBreak);
break;
}
default:
@ -118,7 +119,7 @@ namespace Avalonia.Media.TextFormatting
/// <param name="textRuns">The text run's.</param>
/// <param name="length">The length to split at.</param>
/// <returns>The split text runs.</returns>
internal static SplitTextRunsResult SplitTextRuns(IReadOnlyList<ShapedTextCharacters> textRuns, int length)
internal static SplitTextRunsResult SplitTextRuns(List<ShapedTextCharacters> textRuns, int length)
{
var currentLength = 0;
@ -134,13 +135,13 @@ namespace Avalonia.Media.TextFormatting
var firstCount = currentRun.GlyphRun.Characters.Length >= 1 ? i + 1 : i;
var first = new ShapedTextCharacters[firstCount];
var first = new List<ShapedTextCharacters>(firstCount);
if (firstCount > 1)
{
for (var j = 0; j < i; j++)
{
first[j] = textRuns[j];
first.Add(textRuns[j]);
}
}
@ -148,7 +149,7 @@ namespace Avalonia.Media.TextFormatting
if (currentLength + currentRun.GlyphRun.Characters.Length == length)
{
var second = new ShapedTextCharacters[secondCount];
var second = new List<ShapedTextCharacters>(secondCount);
var offset = currentRun.GlyphRun.Characters.Length > 1 ? 1 : 0;
@ -156,11 +157,11 @@ namespace Avalonia.Media.TextFormatting
{
for (var j = 0; j < secondCount; j++)
{
second[j] = textRuns[i + j + offset];
second.Add(textRuns[i + j + offset]);
}
}
first[i] = currentRun;
first.Add(currentRun);
return new SplitTextRunsResult(first, second);
}
@ -168,22 +169,22 @@ namespace Avalonia.Media.TextFormatting
{
secondCount++;
var second = new ShapedTextCharacters[secondCount];
var second = new List<ShapedTextCharacters>(secondCount);
var split = currentRun.Split(length - currentLength);
first.Add(split.First);
second.Add(split.Second);
if (secondCount > 0)
{
for (var j = 1; j < secondCount; j++)
{
second[j] = textRuns[i + j];
second.Add(textRuns[i + j]);
}
}
var split = currentRun.Split(length - currentLength);
first[i] = split.First;
second[0] = split.Second;
return new SplitTextRunsResult(first, second);
}
}
@ -201,7 +202,7 @@ namespace Avalonia.Media.TextFormatting
/// <returns>
/// The formatted text runs.
/// </returns>
private static IReadOnlyList<ShapedTextCharacters> FetchTextRuns(ITextSource textSource,
private static List<ShapedTextCharacters> FetchTextRuns(ITextSource textSource,
int firstTextSourceIndex, TextLineBreak previousLineBreak, out TextLineBreak nextLineBreak)
{
nextLineBreak = default;
@ -212,8 +213,10 @@ namespace Avalonia.Media.TextFormatting
if (previousLineBreak != null)
{
foreach (var shapedCharacters in previousLineBreak.RemainingCharacters)
for (var index = 0; index < previousLineBreak.RemainingCharacters.Count; index++)
{
var shapedCharacters = previousLineBreak.RemainingCharacters[index];
if (shapedCharacters == null)
{
continue;
@ -225,6 +228,14 @@ namespace Avalonia.Media.TextFormatting
{
var splitResult = SplitTextRuns(textRuns, currentLength + runLineBreak.PositionWrap);
if (++index < previousLineBreak.RemainingCharacters.Count)
{
for (; index < previousLineBreak.RemainingCharacters.Count; index++)
{
splitResult.Second.Add(previousLineBreak.RemainingCharacters[index]);
}
}
nextLineBreak = new TextLineBreak(splitResult.Second);
return splitResult.First;
@ -323,9 +334,10 @@ namespace Avalonia.Media.TextFormatting
/// <param name="textRange">The text range that is covered by the text runs.</param>
/// <param name="paragraphWidth">The paragraph width.</param>
/// <param name="paragraphProperties">The text paragraph properties.</param>
/// <param name="currentLineBreak">The current line break if the line was explicitly broken.</param>
/// <returns>The wrapped text line.</returns>
private static TextLine PerformTextWrapping(IReadOnlyList<ShapedTextCharacters> textRuns, TextRange textRange,
double paragraphWidth, TextParagraphProperties paragraphProperties)
private static TextLine PerformTextWrapping(List<ShapedTextCharacters> textRuns, TextRange textRange,
double paragraphWidth, TextParagraphProperties paragraphProperties, TextLineBreak currentLineBreak)
{
var availableWidth = paragraphWidth;
var currentWidth = 0.0;
@ -388,8 +400,22 @@ namespace Avalonia.Media.TextFormatting
var textLineMetrics = TextLineMetrics.Create(splitResult.First,
new TextRange(textRange.Start, currentLength), paragraphWidth, paragraphProperties);
var lineBreak = splitResult.Second != null && splitResult.Second.Count > 0 ?
new TextLineBreak(splitResult.Second) :
var remainingCharacters = splitResult.Second;
if (currentLineBreak?.RemainingCharacters != null)
{
if (remainingCharacters != null)
{
remainingCharacters.AddRange(currentLineBreak.RemainingCharacters);
}
else
{
remainingCharacters = new List<ShapedTextCharacters>(currentLineBreak.RemainingCharacters);
}
}
var lineBreak = remainingCharacters != null && remainingCharacters.Count > 0 ?
new TextLineBreak(remainingCharacters) :
null;
return new TextLineImpl(splitResult.First, textLineMetrics, lineBreak);
@ -403,7 +429,10 @@ namespace Avalonia.Media.TextFormatting
}
return new TextLineImpl(textRuns,
TextLineMetrics.Create(textRuns, textRange, paragraphWidth, paragraphProperties));
TextLineMetrics.Create(textRuns, textRange, paragraphWidth, paragraphProperties),
currentLineBreak?.RemainingCharacters != null ?
new TextLineBreak(currentLineBreak.RemainingCharacters) :
null);
}
/// <summary>
@ -434,7 +463,7 @@ namespace Avalonia.Media.TextFormatting
internal readonly struct SplitTextRunsResult
{
public SplitTextRunsResult(IReadOnlyList<ShapedTextCharacters> first, IReadOnlyList<ShapedTextCharacters> second)
public SplitTextRunsResult(List<ShapedTextCharacters> first, List<ShapedTextCharacters> second)
{
First = first;
@ -447,7 +476,7 @@ namespace Avalonia.Media.TextFormatting
/// <value>
/// The first text runs.
/// </value>
public IReadOnlyList<ShapedTextCharacters> First { get; }
public List<ShapedTextCharacters> First { get; }
/// <summary>
/// Gets the second text runs.
@ -455,7 +484,7 @@ namespace Avalonia.Media.TextFormatting
/// <value>
/// The second text runs.
/// </value>
public IReadOnlyList<ShapedTextCharacters> Second { get; }
public List<ShapedTextCharacters> Second { get; }
}
private struct TextRunEnumerator

5
src/Avalonia.Visuals/Media/TextFormatting/TextLayout.cs

@ -183,7 +183,10 @@ namespace Avalonia.Media.TextFormatting
var glyphRun = TextShaper.Current.ShapeText(new ReadOnlySlice<char>(s_empty, startingIndex, 1),
properties.Typeface, properties.FontRenderingEmSize, properties.CultureInfo);
var textRuns = new[] { new ShapedTextCharacters(glyphRun, _paragraphProperties.DefaultTextRunProperties) };
var textRuns = new List<ShapedTextCharacters>
{
new ShapedTextCharacters(glyphRun, _paragraphProperties.DefaultTextRunProperties)
};
return new TextLineImpl(textRuns,
TextLineMetrics.Create(textRuns, new TextRange(startingIndex, 1), MaxWidth, _paragraphProperties));

4
src/Avalonia.Visuals/Media/TextFormatting/TextLineImpl.cs

@ -6,9 +6,9 @@ namespace Avalonia.Media.TextFormatting
{
internal class TextLineImpl : TextLine
{
private readonly IReadOnlyList<ShapedTextCharacters> _textRuns;
private readonly List<ShapedTextCharacters> _textRuns;
public TextLineImpl(IReadOnlyList<ShapedTextCharacters> textRuns, TextLineMetrics lineMetrics,
public TextLineImpl(List<ShapedTextCharacters> textRuns, TextLineMetrics lineMetrics,
TextLineBreak lineBreak = null, bool hasCollapsed = false)
{
_textRuns = textRuns;

11
src/Avalonia.Visuals/Rendering/ICustomSimpleHitTest.cs

@ -14,6 +14,17 @@ namespace Avalonia.Rendering
bool HitTest(Point point);
}
/// <summary>
/// Allows customization of hit-testing for all renderers.
/// </summary>
/// <remarks>
/// Note that this interface can only used to make a portion of a control non-hittable, it
/// cannot expand the hittable area of a control.
/// </remarks>
public interface ICustomHitTest : ICustomSimpleHitTest
{
}
public static class CustomSimpleHitTestExtensions
{
public static bool HitTestCustom(this IVisual visual, Point point)

6
src/Avalonia.Visuals/Rendering/SceneGraph/Scene.cs

@ -304,6 +304,12 @@ namespace Avalonia.Rendering.SceneGraph
clipped = !node.GeometryClip.FillContains(controlPoint.Value);
}
if (!clipped && node.Visual is ICustomHitTest custom)
{
var controlPoint = _sceneRoot.Visual.TranslatePoint(_point, node.Visual);
clipped = !custom.HitTest(controlPoint.Value);
}
return !clipped;
}

10
src/Skia/Avalonia.Skia/TextShaperImpl.cs

@ -123,10 +123,7 @@ namespace Avalonia.Skia
return;
}
if (offsetBuffer == null)
{
offsetBuffer = new Vector[glyphPositions.Length];
}
offsetBuffer ??= new Vector[glyphPositions.Length];
var offsetX = position.XOffset * textScale;
@ -138,10 +135,7 @@ namespace Avalonia.Skia
private static void SetAdvance(ReadOnlySpan<GlyphPosition> glyphPositions, int index, double textScale,
ref double[] advanceBuffer)
{
if (advanceBuffer == null)
{
advanceBuffer = new double[glyphPositions.Length];
}
advanceBuffer ??= new double[glyphPositions.Length];
// Depends on direction of layout
// advanceBuffer[index] = buffer.GlyphPositions[index].YAdvance * textScale;

126
tests/Avalonia.Controls.UnitTests/ListBoxTests_Multiple.cs

@ -0,0 +1,126 @@
using System.Linq;
using Avalonia.Controls.Presenters;
using Avalonia.Controls.Templates;
using Avalonia.Input;
using Avalonia.Styling;
using Avalonia.UnitTests;
using Avalonia.VisualTree;
using Xunit;
namespace Avalonia.Controls.UnitTests
{
public class ListBoxTests_Multiple
{
[Fact]
public void Focusing_Item_With_Shift_And_Arrow_Key_Should_Add_To_Selection()
{
var target = new ListBox
{
Template = new FuncControlTemplate(CreateListBoxTemplate),
Items = new[] { "Foo", "Bar", "Baz " },
SelectionMode = SelectionMode.Multiple
};
ApplyTemplate(target);
target.SelectedItem = "Foo";
target.Presenter.Panel.Children[1].RaiseEvent(new GotFocusEventArgs
{
RoutedEvent = InputElement.GotFocusEvent,
NavigationMethod = NavigationMethod.Directional,
KeyModifiers = KeyModifiers.Shift
});
Assert.Equal(new[] { "Foo", "Bar" }, target.SelectedItems);
}
[Fact]
public void Focusing_Item_With_Ctrl_And_Arrow_Key_Should_Add_To_Selection()
{
var target = new ListBox
{
Template = new FuncControlTemplate(CreateListBoxTemplate),
Items = new[] { "Foo", "Bar", "Baz " },
SelectionMode = SelectionMode.Multiple
};
ApplyTemplate(target);
target.SelectedItem = "Foo";
target.Presenter.Panel.Children[1].RaiseEvent(new GotFocusEventArgs
{
RoutedEvent = InputElement.GotFocusEvent,
NavigationMethod = NavigationMethod.Directional,
KeyModifiers = KeyModifiers.Control
});
Assert.Equal(new[] { "Foo", "Bar" }, target.SelectedItems);
}
[Fact]
public void Focusing_Selected_Item_With_Ctrl_And_Arrow_Key_Should_Remove_From_Selection()
{
var target = new ListBox
{
Template = new FuncControlTemplate(CreateListBoxTemplate),
Items = new[] { "Foo", "Bar", "Baz " },
SelectionMode = SelectionMode.Multiple
};
ApplyTemplate(target);
target.SelectedItems.Add("Foo");
target.SelectedItems.Add("Bar");
target.Presenter.Panel.Children[0].RaiseEvent(new GotFocusEventArgs
{
RoutedEvent = InputElement.GotFocusEvent,
NavigationMethod = NavigationMethod.Directional,
KeyModifiers = KeyModifiers.Control
});
Assert.Equal(new[] { "Bar" }, target.SelectedItems);
}
private Control CreateListBoxTemplate(ITemplatedControl parent, INameScope scope)
{
return new ScrollViewer
{
Template = new FuncControlTemplate(CreateScrollViewerTemplate),
Content = new ItemsPresenter
{
Name = "PART_ItemsPresenter",
[~ItemsPresenter.ItemsProperty] = parent.GetObservable(ItemsControl.ItemsProperty).ToBinding(),
}.RegisterInNameScope(scope)
};
}
private Control CreateScrollViewerTemplate(ITemplatedControl parent, INameScope scope)
{
return new ScrollContentPresenter
{
Name = "PART_ContentPresenter",
[~ContentPresenter.ContentProperty] =
parent.GetObservable(ContentControl.ContentProperty).ToBinding(),
}.RegisterInNameScope(scope);
}
private void ApplyTemplate(ListBox target)
{
// Apply the template to the ListBox itself.
target.ApplyTemplate();
// Then to its inner ScrollViewer.
var scrollViewer = (ScrollViewer)target.GetVisualChildren().Single();
scrollViewer.ApplyTemplate();
// Then make the ScrollViewer create its child.
((ContentPresenter)scrollViewer.Presenter).UpdateChild();
// Now the ItemsPresenter should be reigstered, so apply its template.
target.Presenter.ApplyTemplate();
}
}
}

18
tests/Avalonia.Controls.UnitTests/Platform/DefaultMenuInteractionHandlerTests.cs

@ -124,6 +124,24 @@ namespace Avalonia.Controls.UnitTests.Platform
Assert.True(e.Handled);
}
[Fact]
public void Click_On_TopLevel_Calls_MainMenu_Open()
{
var target = new DefaultMenuInteractionHandler(false);
var menu = new Mock<IMainMenu>();
menu.As<IMenuElement>();
var item = Mock.Of<IMenuItem>(x =>
x.IsTopLevel == true &&
x.HasSubMenu == true &&
x.Parent == menu.Object);
var e = CreatePressed(item);
target.PointerPressed(item, e);
menu.Verify(x => x.Open());
}
[Fact]
public void Click_On_Open_TopLevel_Menu_Closes_Menu()
{

59
tests/Avalonia.Controls.UnitTests/Primitives/PopupTests.cs

@ -14,6 +14,7 @@ using Avalonia.UnitTests;
using Avalonia.VisualTree;
using Xunit;
using Avalonia.Input;
using Avalonia.Rendering;
namespace Avalonia.Controls.UnitTests.Primitives
{
@ -349,52 +350,48 @@ namespace Avalonia.Controls.UnitTests.Primitives
}
[Fact]
public void StaysOpen_False_Should_Not_Handle_Closing_Click()
public void OverlayDismissEventPassThrough_Should_Pass_Event_To_Window_Contents()
{
using (CreateServices())
{
var window = PreparedWindow();
var renderer = new Mock<IRenderer>();
var platform = AvaloniaLocator.Current.GetService<IWindowingPlatform>();
var windowImpl = Mock.Get(platform.CreateWindow());
windowImpl.Setup(x => x.CreateRenderer(It.IsAny<IRenderRoot>())).Returns(renderer.Object);
var window = new Window(windowImpl.Object);
window.ApplyTemplate();
var target = new Popup()
{
PlacementTarget = window ,
StaysOpen = false,
IsLightDismissEnabled = true,
OverlayDismissEventPassThrough = true,
};
target.Open();
var e = CreatePointerPressedEventArgs(window);
window.RaiseEvent(e);
var raised = 0;
var border = new Border();
window.Content = border;
Assert.False(e.Handled);
}
}
renderer.Setup(x =>
x.HitTestFirst(new Point(10, 15), window, It.IsAny<Func<IVisual, bool>>()))
.Returns(border);
[Fact]
public void Should_Pass_Closing_Click_To_Closed_Event()
{
using (CreateServices())
{
var window = PreparedWindow();
var target = new Popup()
border.PointerPressed += (s, e) =>
{
PlacementTarget = window,
StaysOpen = false,
Assert.Same(border, e.Source);
++raised;
};
target.Open();
Assert.True(target.IsOpen);
var press = CreatePointerPressedEventArgs(window);
var raised = 0;
target.Closed += (s, e) =>
{
Assert.Same(press, e.CloseEvent);
++raised;
};
window.RaiseEvent(press);
var e = CreatePointerPressedEventArgs(window, new Point(10, 15));
var overlay = LightDismissOverlayLayer.GetLightDismissOverlayLayer(window);
overlay.RaiseEvent(e);
Assert.Equal(1, raised);
Assert.False(target.IsOpen);
}
}
@ -410,14 +407,14 @@ namespace Avalonia.Controls.UnitTests.Primitives
})));
}
private PointerPressedEventArgs CreatePointerPressedEventArgs(Window source)
private PointerPressedEventArgs CreatePointerPressedEventArgs(Window source, Point p)
{
var pointer = new Pointer(Pointer.GetNextFreeId(), PointerType.Mouse, true);
return new PointerPressedEventArgs(
source,
pointer,
source,
default,
p,
0,
new PointerPointProperties(RawInputModifiers.None, PointerUpdateKind.LeftButtonPressed),
KeyModifiers.None);

14
tests/Avalonia.Controls.UnitTests/Utils/SelectedItemsSyncTests.cs

@ -208,6 +208,20 @@ namespace Avalonia.Controls.UnitTests.Utils
target.SetItems(new[] { "foo", "bar", "baz" }));
}
[Fact]
public void Selected_Items_Can_Be_Set_Before_SelectionModel_Source()
{
var model = new SelectionModel();
var target = new SelectedItemsSync(model);
var items = new AvaloniaList<string> { "foo", "bar", "baz" };
var selectedItems = new AvaloniaList<string> { "bar" };
target.SetItems(selectedItems);
model.Source = items;
Assert.Equal(new IndexPath(1), model.SelectedIndex);
}
private static SelectedItemsSync CreateTarget(
IEnumerable<string> items = null)
{

1
tests/Avalonia.UnitTests/MockWindowingPlatform.cs

@ -3,6 +3,7 @@ using Avalonia.Controls.Primitives.PopupPositioning;
using Avalonia.Input;
using Moq;
using Avalonia.Platform;
using Avalonia.Rendering;
namespace Avalonia.UnitTests
{

Loading…
Cancel
Save