diff --git a/samples/ControlCatalog/Pages/AutoCompleteBoxPage.xaml b/samples/ControlCatalog/Pages/AutoCompleteBoxPage.xaml index f90a0c4658..a49616e543 100644 --- a/samples/ControlCatalog/Pages/AutoCompleteBoxPage.xaml +++ b/samples/ControlCatalog/Pages/AutoCompleteBoxPage.xaml @@ -51,6 +51,11 @@ Width="200" Margin="0,0,0,8" FilterMode="None"/> + + diff --git a/samples/ControlCatalog/Pages/AutoCompleteBoxPage.xaml.cs b/samples/ControlCatalog/Pages/AutoCompleteBoxPage.xaml.cs index f9d6a72a3a..574cc79a7d 100644 --- a/samples/ControlCatalog/Pages/AutoCompleteBoxPage.xaml.cs +++ b/samples/ControlCatalog/Pages/AutoCompleteBoxPage.xaml.cs @@ -92,13 +92,28 @@ namespace ControlCatalog.Pages } public StateData[] States { get; private set; } + private LinkedList[] BuildAllSentences() + { + return new string[] + { + "Hello world", + "No this is Patrick", + "Never gonna give you up", + "How does one patch KDE2 under FreeBSD" + } + .Select(x => new LinkedList(x.Split(' '))) + .ToArray(); + } + public LinkedList[] Sentences { get; private set; } + public AutoCompleteBoxPage() { this.InitializeComponent(); States = BuildAllStates(); + Sentences = BuildAllSentences(); - foreach (AutoCompleteBox box in GetAllAutoCompleteBox()) + foreach (AutoCompleteBox box in GetAllAutoCompleteBox().Where(x => x.Name != "CustomAutocompleteBox")) { box.Items = States; } @@ -116,6 +131,11 @@ namespace ControlCatalog.Pages var asyncBox = this.FindControl("AsyncBox"); asyncBox.AsyncPopulator = PopulateAsync; + + var customAutocompleteBox = this.FindControl("CustomAutocompleteBox"); + customAutocompleteBox.Items = Sentences.SelectMany(x => x); + customAutocompleteBox.TextFilter = LastWordContains; + customAutocompleteBox.TextSelector = AppendWord; } private IEnumerable GetAllAutoCompleteBox() { @@ -137,6 +157,42 @@ namespace ControlCatalog.Pages .ToList(); } + private bool LastWordContains(string searchText, string item) + { + var words = searchText.Split(' '); + var options = Sentences.Select(x => x.First).ToArray(); + for (var i = 0; i < words.Length; ++i) + { + var word = words[i]; + for (var j = 0; j < options.Length; ++j) + { + var option = options[j]; + if (option == null) + continue; + + if (i == words.Length - 1) + { + options[j] = option.Value.ToLower().Contains(word.ToLower()) ? option : null; + } + else + { + options[j] = option.Value.Equals(word, StringComparison.InvariantCultureIgnoreCase) ? option.Next : null; + } + } + } + + return options.Any(x => x != null && x.Value == item); + } + private string AppendWord(string text, string item) + { + string[] parts = text.Split(' '); + if (parts.Length == 0) + return item; + + parts[parts.Length - 1] = item; + return string.Join(" ", parts); + } + private void InitializeComponent() { AvaloniaXamlLoader.Load(this); diff --git a/samples/RenderDemo/MainWindow.xaml b/samples/RenderDemo/MainWindow.xaml index 770960d7c4..93fbe5e412 100644 --- a/samples/RenderDemo/MainWindow.xaml +++ b/samples/RenderDemo/MainWindow.xaml @@ -3,8 +3,8 @@ x:Class="RenderDemo.MainWindow" Title="AvaloniaUI Rendering Test" xmlns:pages="clr-namespace:RenderDemo.Pages" - Width="800" - Height="600"> + Width="{Binding Width, Mode=TwoWay}" + Height="{Binding Height, Mode=TwoWay}"> @@ -24,6 +24,10 @@ + + + diff --git a/samples/RenderDemo/ViewModels/MainWindowViewModel.cs b/samples/RenderDemo/ViewModels/MainWindowViewModel.cs index d2d789a687..eda5e80530 100644 --- a/samples/RenderDemo/ViewModels/MainWindowViewModel.cs +++ b/samples/RenderDemo/ViewModels/MainWindowViewModel.cs @@ -1,5 +1,6 @@ -using System; -using System.Reactive; +using System.Reactive; +using System.Threading.Tasks; + using ReactiveUI; namespace RenderDemo.ViewModels @@ -8,26 +9,61 @@ namespace RenderDemo.ViewModels { private bool drawDirtyRects = false; private bool drawFps = true; + private double width = 800; + private double height = 600; public MainWindowViewModel() { ToggleDrawDirtyRects = ReactiveCommand.Create(() => DrawDirtyRects = !DrawDirtyRects); ToggleDrawFps = ReactiveCommand.Create(() => DrawFps = !DrawFps); + ResizeWindow = ReactiveCommand.CreateFromTask(ResizeWindowAsync); } public bool DrawDirtyRects { - get { return drawDirtyRects; } - set { this.RaiseAndSetIfChanged(ref drawDirtyRects, value); } + get => drawDirtyRects; + set => this.RaiseAndSetIfChanged(ref drawDirtyRects, value); } public bool DrawFps { - get { return drawFps; } - set { this.RaiseAndSetIfChanged(ref drawFps, value); } + get => drawFps; + set => this.RaiseAndSetIfChanged(ref drawFps, value); + } + + public double Width + { + get => width; + set => this.RaiseAndSetIfChanged(ref width, value); + } + + public double Height + { + get => height; + set => this.RaiseAndSetIfChanged(ref height, value); } public ReactiveCommand ToggleDrawDirtyRects { get; } public ReactiveCommand ToggleDrawFps { get; } + public ReactiveCommand ResizeWindow { get; } + + private async Task ResizeWindowAsync() + { + for (int i = 0; i < 30; i++) + { + Width += 10; + Height += 5; + await Task.Delay(10); + } + + await Task.Delay(10); + + for (int i = 0; i < 30; i++) + { + Width -= 10; + Height -= 5; + await Task.Delay(10); + } + } } } diff --git a/src/Avalonia.Controls.DataGrid/DataGrid.cs b/src/Avalonia.Controls.DataGrid/DataGrid.cs index 9ca0b91523..7c57ea3db9 100644 --- a/src/Avalonia.Controls.DataGrid/DataGrid.cs +++ b/src/Avalonia.Controls.DataGrid/DataGrid.cs @@ -24,12 +24,14 @@ using Avalonia.Input.Platform; using System.ComponentModel.DataAnnotations; using Avalonia.Controls.Utils; using Avalonia.Layout; +using Avalonia.Controls.Metadata; namespace Avalonia.Controls { /// /// Displays data in a customizable grid. /// + [PseudoClasses(":invalid")] public partial class DataGrid : TemplatedControl { private const string DATAGRID_elementRowsPresenterName = "PART_RowsPresenter"; diff --git a/src/Avalonia.Controls.DataGrid/DataGridCell.cs b/src/Avalonia.Controls.DataGrid/DataGridCell.cs index e5fbfa1a81..445dc541a7 100644 --- a/src/Avalonia.Controls.DataGrid/DataGridCell.cs +++ b/src/Avalonia.Controls.DataGrid/DataGridCell.cs @@ -3,6 +3,7 @@ // Please see http://go.microsoft.com/fwlink/?LinkID=131993 for details. // All other rights reserved. +using Avalonia.Controls.Metadata; using Avalonia.Controls.Primitives; using Avalonia.Controls.Shapes; using Avalonia.Input; @@ -12,6 +13,7 @@ namespace Avalonia.Controls /// /// Represents an individual cell. /// + [PseudoClasses(":selected", ":current", ":edited", ":invalid")] public class DataGridCell : ContentControl { private const string DATAGRIDCELL_elementRightGridLine = "PART_RightGridLine"; diff --git a/src/Avalonia.Controls.DataGrid/DataGridColumnHeader.cs b/src/Avalonia.Controls.DataGrid/DataGridColumnHeader.cs index 25aae99942..856d1f6566 100644 --- a/src/Avalonia.Controls.DataGrid/DataGridColumnHeader.cs +++ b/src/Avalonia.Controls.DataGrid/DataGridColumnHeader.cs @@ -14,12 +14,14 @@ using Avalonia.Utilities; using System; using Avalonia.Controls.Utils; using Avalonia.Controls.Mixins; +using Avalonia.Controls.Metadata; namespace Avalonia.Controls { /// /// Represents an individual column header. /// + [PseudoClasses(":dragIndicator", ":pressed", ":sortascending", ":sortdescending")] public class DataGridColumnHeader : ContentControl { private enum DragMode diff --git a/src/Avalonia.Controls.DataGrid/DataGridRow.cs b/src/Avalonia.Controls.DataGrid/DataGridRow.cs index d5ce8dba75..c3562c53a4 100644 --- a/src/Avalonia.Controls.DataGrid/DataGridRow.cs +++ b/src/Avalonia.Controls.DataGrid/DataGridRow.cs @@ -3,6 +3,7 @@ // Please see http://go.microsoft.com/fwlink/?LinkID=131993 for details. // All other rights reserved. +using Avalonia.Controls.Metadata; using Avalonia.Controls.Primitives; using Avalonia.Controls.Shapes; using Avalonia.Controls.Templates; @@ -20,6 +21,7 @@ namespace Avalonia.Controls /// /// Represents a row. /// + [PseudoClasses(":selected", ":editing", ":invalid")] public class DataGridRow : TemplatedControl { diff --git a/src/Avalonia.Controls.DataGrid/DataGridRowGroupHeader.cs b/src/Avalonia.Controls.DataGrid/DataGridRowGroupHeader.cs index 0833247439..1e03b134b1 100644 --- a/src/Avalonia.Controls.DataGrid/DataGridRowGroupHeader.cs +++ b/src/Avalonia.Controls.DataGrid/DataGridRowGroupHeader.cs @@ -3,6 +3,7 @@ // Please see http://go.microsoft.com/fwlink/?LinkID=131993 for details. // All other rights reserved. +using Avalonia.Controls.Metadata; using Avalonia.Controls.Mixins; using Avalonia.Controls.Primitives; using Avalonia.Input; @@ -13,6 +14,7 @@ using System.Reactive.Linq; namespace Avalonia.Controls { + [PseudoClasses(":pressed", ":current", ":expanded")] public class DataGridRowGroupHeader : TemplatedControl { private const string DATAGRIDROWGROUPHEADER_expanderButton = "ExpanderButton"; diff --git a/src/Avalonia.Controls.DataGrid/DataGridRowHeader.cs b/src/Avalonia.Controls.DataGrid/DataGridRowHeader.cs index 8f8b1742ba..0cd3589a57 100644 --- a/src/Avalonia.Controls.DataGrid/DataGridRowHeader.cs +++ b/src/Avalonia.Controls.DataGrid/DataGridRowHeader.cs @@ -3,6 +3,7 @@ // Please see http://go.microsoft.com/fwlink/?LinkID=131993 for details. // All other rights reserved. +using Avalonia.Controls.Metadata; using Avalonia.Input; using Avalonia.Media; using System.Diagnostics; @@ -12,6 +13,7 @@ namespace Avalonia.Controls.Primitives /// /// Represents an individual row header. /// + [PseudoClasses(":invalid", ":selected", ":editing", ":current")] public class DataGridRowHeader : ContentControl { private const string DATAGRIDROWHEADER_elementRootName = "PART_Root"; diff --git a/src/Avalonia.Controls/AutoCompleteBox.cs b/src/Avalonia.Controls/AutoCompleteBox.cs index c164f282e8..bfd633c947 100644 --- a/src/Avalonia.Controls/AutoCompleteBox.cs +++ b/src/Avalonia.Controls/AutoCompleteBox.cs @@ -14,6 +14,7 @@ using System.Reactive.Linq; using System.Threading; using System.Threading.Tasks; using Avalonia.Collections; +using Avalonia.Controls.Metadata; using Avalonia.Controls.Primitives; using Avalonia.Controls.Templates; using Avalonia.Controls.Utils; @@ -30,6 +31,7 @@ namespace Avalonia.Controls /// /// event. /// + [PseudoClasses(":dropdownopen")] public class PopulatedEventArgs : EventArgs { /// @@ -225,6 +227,27 @@ namespace Avalonia.Controls Custom = 13, } + /// + /// Represents the selector used by the + /// control to + /// determine how the specified text should be modified with an item. + /// + /// + /// Modified text that will be used by the + /// . + /// + /// The string used as the basis for filtering. + /// + /// The selected item that should be combined with the + /// parameter. + /// + /// + /// The type used for filtering the + /// . + /// This type can be either a string or an object. + /// + public delegate string AutoCompleteSelector(string search, T item); + /// /// Represents a control that provides a text box for user input and a /// drop-down that contains possible matches based on the input in the text @@ -362,6 +385,9 @@ namespace Avalonia.Controls private AutoCompleteFilterPredicate _itemFilter; private AutoCompleteFilterPredicate _textFilter = AutoCompleteSearch.GetFilter(AutoCompleteFilterMode.StartsWith); + private AutoCompleteSelector _itemSelector; + private AutoCompleteSelector _textSelector; + public static readonly RoutedEvent SelectionChangedEvent = RoutedEvent.Register(nameof(SelectionChanged), RoutingStrategies.Bubble, typeof(AutoCompleteBox)); @@ -528,6 +554,34 @@ namespace Avalonia.Controls (o, v) => o.TextFilter = v, unsetValue: AutoCompleteSearch.GetFilter(AutoCompleteFilterMode.StartsWith)); + /// + /// Identifies the + /// + /// dependency property. + /// + /// The identifier for the + /// + /// dependency property. + public static readonly DirectProperty> ItemSelectorProperty = + AvaloniaProperty.RegisterDirect>( + nameof(ItemSelector), + o => o.ItemSelector, + (o, v) => o.ItemSelector = v); + + /// + /// Identifies the + /// + /// dependency property. + /// + /// The identifier for the + /// + /// dependency property. + public static readonly DirectProperty> TextSelectorProperty = + AvaloniaProperty.RegisterDirect>( + nameof(TextSelector), + o => o.TextSelector, + (o, v) => o.TextSelector = v); + /// /// Identifies the /// @@ -1061,6 +1115,40 @@ namespace Avalonia.Controls set { SetAndRaise(TextFilterProperty, ref _textFilter, value); } } + /// + /// Gets or sets the custom method that combines the user-entered + /// text and one of the items specified by the + /// . + /// + /// + /// The custom method that combines the user-entered + /// text and one of the items specified by the + /// . + /// + public AutoCompleteSelector ItemSelector + { + get { return _itemSelector; } + set { SetAndRaise(ItemSelectorProperty, ref _itemSelector, value); } + } + + /// + /// Gets or sets the custom method that combines the user-entered + /// text and one of the items specified by the + /// + /// in a text-based way. + /// + /// + /// The custom method that combines the user-entered + /// text and one of the items specified by the + /// + /// in a text-based way. + /// + public AutoCompleteSelector TextSelector + { + get { return _textSelector; } + set { SetAndRaise(TextSelectorProperty, ref _textSelector, value); } + } + public Func>> AsyncPopulator { get { return _asyncPopulator; } @@ -2329,6 +2417,14 @@ namespace Avalonia.Controls { text = SearchText; } + else if (TextSelector != null) + { + text = TextSelector(SearchText, FormatValue(newItem, true)); + } + else if (ItemSelector != null) + { + text = ItemSelector(SearchText, newItem); + } else { text = FormatValue(newItem, true); diff --git a/src/Avalonia.Controls/Button.cs b/src/Avalonia.Controls/Button.cs index b54eb2ac57..e94d00b2ff 100644 --- a/src/Avalonia.Controls/Button.cs +++ b/src/Avalonia.Controls/Button.cs @@ -1,6 +1,7 @@ using System; using System.Linq; using System.Windows.Input; +using Avalonia.Controls.Metadata; using Avalonia.Data; using Avalonia.Input; using Avalonia.Interactivity; @@ -28,6 +29,7 @@ namespace Avalonia.Controls /// /// A button control. /// + [PseudoClasses(":pressed")] public class Button : ContentControl { /// diff --git a/src/Avalonia.Controls/ButtonSpinner.cs b/src/Avalonia.Controls/ButtonSpinner.cs index 44f66d397a..5fe2cf3704 100644 --- a/src/Avalonia.Controls/ButtonSpinner.cs +++ b/src/Avalonia.Controls/ButtonSpinner.cs @@ -1,4 +1,5 @@ using System; +using Avalonia.Controls.Metadata; using Avalonia.Controls.Primitives; using Avalonia.Data; using Avalonia.Input; @@ -15,6 +16,7 @@ namespace Avalonia.Controls /// /// Represents a spinner control that includes two Buttons. /// + [PseudoClasses(":left", ":right")] public class ButtonSpinner : Spinner { /// diff --git a/src/Avalonia.Controls/Calendar/CalendarButton.cs b/src/Avalonia.Controls/Calendar/CalendarButton.cs index 80370df145..76af933b55 100644 --- a/src/Avalonia.Controls/Calendar/CalendarButton.cs +++ b/src/Avalonia.Controls/Calendar/CalendarButton.cs @@ -3,6 +3,7 @@ // Please see http://go.microsoft.com/fwlink/?LinkID=131993 for details. // All other rights reserved. +using Avalonia.Controls.Metadata; using Avalonia.Input; using System; @@ -12,6 +13,7 @@ namespace Avalonia.Controls.Primitives /// Represents a button on a /// . /// + [PseudoClasses(":selected", ":inactive", ":btnfocused")] public sealed class CalendarButton : Button { /// diff --git a/src/Avalonia.Controls/Calendar/CalendarDayButton.cs b/src/Avalonia.Controls/Calendar/CalendarDayButton.cs index 3a39bd10fa..d5748bb9e4 100644 --- a/src/Avalonia.Controls/Calendar/CalendarDayButton.cs +++ b/src/Avalonia.Controls/Calendar/CalendarDayButton.cs @@ -5,10 +5,12 @@ using System; using System.Globalization; +using Avalonia.Controls.Metadata; using Avalonia.Input; namespace Avalonia.Controls.Primitives { + [PseudoClasses(":pressed", ":disabled", ":selected", ":inactive", ":today", ":blackout", ":dayfocused")] public sealed class CalendarDayButton : Button { /// diff --git a/src/Avalonia.Controls/Calendar/CalendarItem.cs b/src/Avalonia.Controls/Calendar/CalendarItem.cs index 0be7c4f67e..e9ea942142 100644 --- a/src/Avalonia.Controls/Calendar/CalendarItem.cs +++ b/src/Avalonia.Controls/Calendar/CalendarItem.cs @@ -7,6 +7,7 @@ using System; using System.Collections.Generic; using System.Diagnostics; using System.Globalization; +using Avalonia.Controls.Metadata; using Avalonia.Data; using Avalonia.Input; using Avalonia.Interactivity; @@ -18,6 +19,7 @@ namespace Avalonia.Controls.Primitives /// Represents the currently displayed month or year on a /// . /// + [PseudoClasses(":calendardisabled")] public sealed class CalendarItem : TemplatedControl { /// diff --git a/src/Avalonia.Controls/Chrome/CaptionButtons.cs b/src/Avalonia.Controls/Chrome/CaptionButtons.cs index a86cbc271b..cd60130c5b 100644 --- a/src/Avalonia.Controls/Chrome/CaptionButtons.cs +++ b/src/Avalonia.Controls/Chrome/CaptionButtons.cs @@ -1,5 +1,6 @@ using System; using System.Reactive.Disposables; +using Avalonia.Controls.Metadata; using Avalonia.Controls.Primitives; #nullable enable @@ -9,6 +10,7 @@ namespace Avalonia.Controls.Chrome /// /// Draws window minimize / maximize / close buttons in a when managed client decorations are enabled. /// + [PseudoClasses(":minimized", ":normal", ":maximized", ":fullscreen")] public class CaptionButtons : TemplatedControl { private CompositeDisposable? _disposables; diff --git a/src/Avalonia.Controls/Chrome/TitleBar.cs b/src/Avalonia.Controls/Chrome/TitleBar.cs index c0c8076dd8..fbddb06952 100644 --- a/src/Avalonia.Controls/Chrome/TitleBar.cs +++ b/src/Avalonia.Controls/Chrome/TitleBar.cs @@ -1,5 +1,6 @@ using System; using System.Reactive.Disposables; +using Avalonia.Controls.Metadata; using Avalonia.Controls.Primitives; #nullable enable @@ -9,6 +10,7 @@ namespace Avalonia.Controls.Chrome /// /// Draws a titlebar when managed client decorations are enabled. /// + [PseudoClasses(":minimized", ":normal", ":maximized", ":fullscreen")] public class TitleBar : TemplatedControl { private CompositeDisposable? _disposables; diff --git a/src/Avalonia.Controls/DataValidationErrors.cs b/src/Avalonia.Controls/DataValidationErrors.cs index dfe9a16532..3c64691816 100644 --- a/src/Avalonia.Controls/DataValidationErrors.cs +++ b/src/Avalonia.Controls/DataValidationErrors.cs @@ -2,6 +2,7 @@ using System.Collections.Generic; using System.Linq; using System.Reactive.Linq; +using Avalonia.Controls.Metadata; using Avalonia.Controls.Templates; using Avalonia.Data; @@ -14,6 +15,7 @@ namespace Avalonia.Controls /// /// You will probably only want to create instances inside of control templates. /// + [PseudoClasses(":error")] public class DataValidationErrors : ContentControl { /// diff --git a/src/Avalonia.Controls/DateTimePickers/DatePicker.cs b/src/Avalonia.Controls/DateTimePickers/DatePicker.cs index a41c159980..8d893154eb 100644 --- a/src/Avalonia.Controls/DateTimePickers/DatePicker.cs +++ b/src/Avalonia.Controls/DateTimePickers/DatePicker.cs @@ -1,4 +1,5 @@ -using Avalonia.Controls.Primitives; +using Avalonia.Controls.Metadata; +using Avalonia.Controls.Primitives; using Avalonia.Controls.Shapes; using Avalonia.Controls.Templates; using Avalonia.Interactivity; @@ -11,6 +12,7 @@ namespace Avalonia.Controls /// /// A control to allow the user to select a date /// + [PseudoClasses(":hasnodate")] public class DatePicker : TemplatedControl { /// diff --git a/src/Avalonia.Controls/DateTimePickers/TimePicker.cs b/src/Avalonia.Controls/DateTimePickers/TimePicker.cs index e54da1fb3a..e4ff5e9e5b 100644 --- a/src/Avalonia.Controls/DateTimePickers/TimePicker.cs +++ b/src/Avalonia.Controls/DateTimePickers/TimePicker.cs @@ -1,4 +1,5 @@ -using Avalonia.Controls.Primitives; +using Avalonia.Controls.Metadata; +using Avalonia.Controls.Primitives; using Avalonia.Controls.Shapes; using Avalonia.Controls.Templates; using System; @@ -9,6 +10,7 @@ namespace Avalonia.Controls /// /// A control to allow the user to select a time /// + [PseudoClasses(":hasnotime")] public class TimePicker : TemplatedControl { /// diff --git a/src/Avalonia.Controls/Expander.cs b/src/Avalonia.Controls/Expander.cs index 43882b70c8..9ff2e41fa9 100644 --- a/src/Avalonia.Controls/Expander.cs +++ b/src/Avalonia.Controls/Expander.cs @@ -1,6 +1,6 @@ using Avalonia.Animation; +using Avalonia.Controls.Metadata; using Avalonia.Controls.Primitives; -using Avalonia.Data; namespace Avalonia.Controls { @@ -12,6 +12,7 @@ namespace Avalonia.Controls Right } + [PseudoClasses(":expanded", ":up", ":down", ":left", ":right")] public class Expander : HeaderedContentControl { public static readonly StyledProperty ContentTransitionProperty = diff --git a/src/Avalonia.Controls/ItemsControl.cs b/src/Avalonia.Controls/ItemsControl.cs index a3dfe33641..3aec06e4eb 100644 --- a/src/Avalonia.Controls/ItemsControl.cs +++ b/src/Avalonia.Controls/ItemsControl.cs @@ -4,6 +4,7 @@ using System.Collections.Generic; using System.Collections.Specialized; using Avalonia.Collections; using Avalonia.Controls.Generators; +using Avalonia.Controls.Metadata; using Avalonia.Controls.Presenters; using Avalonia.Controls.Primitives; using Avalonia.Controls.Templates; @@ -18,6 +19,7 @@ namespace Avalonia.Controls /// /// Displays a collection of items. /// + [PseudoClasses(":empty", ":singleitem")] public class ItemsControl : TemplatedControl, IItemsPresenterHost, ICollectionChangedListener { /// diff --git a/src/Avalonia.Controls/ListBoxItem.cs b/src/Avalonia.Controls/ListBoxItem.cs index e04c79987f..4fe5f4de40 100644 --- a/src/Avalonia.Controls/ListBoxItem.cs +++ b/src/Avalonia.Controls/ListBoxItem.cs @@ -1,3 +1,4 @@ +using Avalonia.Controls.Metadata; using Avalonia.Controls.Mixins; using Avalonia.Input; @@ -6,6 +7,7 @@ namespace Avalonia.Controls /// /// A selectable item in a . /// + [PseudoClasses(":pressed", ":selected")] public class ListBoxItem : ContentControl, ISelectable { /// diff --git a/src/Avalonia.Controls/MenuItem.cs b/src/Avalonia.Controls/MenuItem.cs index b4d3272471..3d8ab3ae48 100644 --- a/src/Avalonia.Controls/MenuItem.cs +++ b/src/Avalonia.Controls/MenuItem.cs @@ -4,6 +4,7 @@ using System.Linq; using System.Reactive.Linq; using System.Windows.Input; using Avalonia.Controls.Generators; +using Avalonia.Controls.Metadata; using Avalonia.Controls.Mixins; using Avalonia.Controls.Primitives; using Avalonia.Controls.Templates; @@ -20,6 +21,7 @@ namespace Avalonia.Controls /// /// A menu item control. /// + [PseudoClasses(":separator", ":icon", ":open", ":pressed", ":selected")] public class MenuItem : HeaderedSelectingItemsControl, IMenuItem, ISelectable { /// diff --git a/src/Avalonia.Controls/Notifications/NotificationCard.cs b/src/Avalonia.Controls/Notifications/NotificationCard.cs index f90746bf06..cdbace3ced 100644 --- a/src/Avalonia.Controls/Notifications/NotificationCard.cs +++ b/src/Avalonia.Controls/Notifications/NotificationCard.cs @@ -1,6 +1,7 @@ using System; using System.Linq; using System.Reactive.Linq; +using Avalonia.Controls.Metadata; using Avalonia.Interactivity; using Avalonia.LogicalTree; @@ -9,6 +10,7 @@ namespace Avalonia.Controls.Notifications /// /// Control that represents and displays a notification. /// + [PseudoClasses(":error", ":information", ":success", ":warning")] public class NotificationCard : ContentControl { private bool _isClosed; diff --git a/src/Avalonia.Controls/Notifications/WindowNotificationManager.cs b/src/Avalonia.Controls/Notifications/WindowNotificationManager.cs index 6d9f6b8b77..8f5c6faf40 100644 --- a/src/Avalonia.Controls/Notifications/WindowNotificationManager.cs +++ b/src/Avalonia.Controls/Notifications/WindowNotificationManager.cs @@ -7,12 +7,14 @@ using Avalonia.Controls.Primitives; using Avalonia.Rendering; using Avalonia.Data; using Avalonia.VisualTree; +using Avalonia.Controls.Metadata; namespace Avalonia.Controls.Notifications { /// /// An that displays notifications in a . /// + [PseudoClasses(":topleft", ":topright", ":bottomleft", ":bottomright")] public class WindowNotificationManager : TemplatedControl, IManagedNotificationManager, ICustomSimpleHitTest { private IList _items; diff --git a/src/Avalonia.Controls/Primitives/PopupPositioning/ManagedPopupPositioner.cs b/src/Avalonia.Controls/Primitives/PopupPositioning/ManagedPopupPositioner.cs index 8c464c7aad..7f1dbdf592 100644 --- a/src/Avalonia.Controls/Primitives/PopupPositioning/ManagedPopupPositioner.cs +++ b/src/Avalonia.Controls/Primitives/PopupPositioning/ManagedPopupPositioner.cs @@ -221,7 +221,7 @@ namespace Avalonia.Controls.Primitives.PopupPositioning if (!FitsInBounds(unconstrainedRect, PopupAnchor.Bottom)) { - unconstrainedRect = unconstrainedRect.WithHeight(bounds.Height - unconstrainedRect.Y); + unconstrainedRect = unconstrainedRect.WithHeight(bounds.Bottom - unconstrainedRect.Y); } if (IsValid(unconstrainedRect)) diff --git a/src/Avalonia.Controls/Primitives/ScrollBar.cs b/src/Avalonia.Controls/Primitives/ScrollBar.cs index 477d24dc99..a7fb7ae08c 100644 --- a/src/Avalonia.Controls/Primitives/ScrollBar.cs +++ b/src/Avalonia.Controls/Primitives/ScrollBar.cs @@ -4,6 +4,7 @@ using Avalonia.Interactivity; using Avalonia.Input; using Avalonia.Layout; using Avalonia.Threading; +using Avalonia.Controls.Metadata; namespace Avalonia.Controls.Primitives { @@ -21,6 +22,7 @@ namespace Avalonia.Controls.Primitives /// /// A scrollbar control. /// + [PseudoClasses(":vertical", ":horizontal")] public class ScrollBar : RangeBase { /// diff --git a/src/Avalonia.Controls/Primitives/Thumb.cs b/src/Avalonia.Controls/Primitives/Thumb.cs index 96810ed01b..348922b71d 100644 --- a/src/Avalonia.Controls/Primitives/Thumb.cs +++ b/src/Avalonia.Controls/Primitives/Thumb.cs @@ -1,9 +1,11 @@ using System; +using Avalonia.Controls.Metadata; using Avalonia.Input; using Avalonia.Interactivity; namespace Avalonia.Controls.Primitives { + [PseudoClasses(":pressed")] public class Thumb : TemplatedControl { public static readonly RoutedEvent DragStartedEvent = diff --git a/src/Avalonia.Controls/Primitives/ToggleButton.cs b/src/Avalonia.Controls/Primitives/ToggleButton.cs index 13031ddad8..f96ca9310d 100644 --- a/src/Avalonia.Controls/Primitives/ToggleButton.cs +++ b/src/Avalonia.Controls/Primitives/ToggleButton.cs @@ -1,4 +1,5 @@ using System; +using Avalonia.Controls.Metadata; using Avalonia.Data; using Avalonia.Interactivity; @@ -7,6 +8,7 @@ namespace Avalonia.Controls.Primitives /// /// Represents a control that a user can select (check) or clear (uncheck). Base class for controls that can switch states. /// + [PseudoClasses(":checked", ":unchecked", ":indeterminate")] public class ToggleButton : Button { /// diff --git a/src/Avalonia.Controls/Primitives/Track.cs b/src/Avalonia.Controls/Primitives/Track.cs index 29e7f28b44..9399f5fb31 100644 --- a/src/Avalonia.Controls/Primitives/Track.cs +++ b/src/Avalonia.Controls/Primitives/Track.cs @@ -4,6 +4,7 @@ // Licensed to The Avalonia Project under MIT License, courtesy of The .NET Foundation. using System; +using Avalonia.Controls.Metadata; using Avalonia.Data; using Avalonia.Input; using Avalonia.Layout; @@ -12,6 +13,7 @@ using Avalonia.Utilities; namespace Avalonia.Controls.Primitives { + [PseudoClasses(":vertical", ":horizontal")] public class Track : Control { public static readonly DirectProperty MinimumProperty = diff --git a/src/Avalonia.Controls/ProgressBar.cs b/src/Avalonia.Controls/ProgressBar.cs index a92f24a050..161f09d9b6 100644 --- a/src/Avalonia.Controls/ProgressBar.cs +++ b/src/Avalonia.Controls/ProgressBar.cs @@ -1,4 +1,5 @@ using System; +using Avalonia.Controls.Metadata; using Avalonia.Controls.Primitives; using Avalonia.Layout; using Avalonia.Media; @@ -8,6 +9,7 @@ namespace Avalonia.Controls /// /// A control used to indicate the progress of an operation. /// + [PseudoClasses(":vertical", ":horizontal", ":indeterminate")] public class ProgressBar : RangeBase { public class ProgressBarTemplateProperties : AvaloniaObject diff --git a/src/Avalonia.Controls/Slider.cs b/src/Avalonia.Controls/Slider.cs index 293cbac82f..6e08e78813 100644 --- a/src/Avalonia.Controls/Slider.cs +++ b/src/Avalonia.Controls/Slider.cs @@ -1,5 +1,6 @@ using System; using Avalonia.Collections; +using Avalonia.Controls.Metadata; using Avalonia.Controls.Mixins; using Avalonia.Controls.Primitives; using Avalonia.Input; @@ -39,6 +40,7 @@ namespace Avalonia.Controls /// /// A control that lets the user select from a range of values by moving a Thumb control along a Track. /// + [PseudoClasses(":vertical", ":horizontal", ":pressed")] public class Slider : RangeBase { /// diff --git a/src/Avalonia.Controls/SplitView.cs b/src/Avalonia.Controls/SplitView.cs index b71858f796..8267efc466 100644 --- a/src/Avalonia.Controls/SplitView.cs +++ b/src/Avalonia.Controls/SplitView.cs @@ -1,4 +1,5 @@ -using Avalonia.Controls.Primitives; +using Avalonia.Controls.Metadata; +using Avalonia.Controls.Primitives; using Avalonia.Input; using Avalonia.Input.Raw; using Avalonia.Interactivity; @@ -73,6 +74,10 @@ namespace Avalonia.Controls /// /// A control with two views: A collapsible pane and an area for content /// + [PseudoClasses(":open", ":closed")] + [PseudoClasses(":compactoverlay", ":compactinline", ":overlay", ":inline")] + [PseudoClasses(":left", ":right")] + [PseudoClasses(":lightdismiss")] public class SplitView : TemplatedControl { /* diff --git a/src/Avalonia.Controls/TabItem.cs b/src/Avalonia.Controls/TabItem.cs index 6320443a13..593643a1eb 100644 --- a/src/Avalonia.Controls/TabItem.cs +++ b/src/Avalonia.Controls/TabItem.cs @@ -1,3 +1,4 @@ +using Avalonia.Controls.Metadata; using Avalonia.Controls.Mixins; using Avalonia.Controls.Primitives; @@ -6,6 +7,7 @@ namespace Avalonia.Controls /// /// An item in a or . /// + [PseudoClasses(":pressed", ":selected")] public class TabItem : HeaderedContentControl, ISelectable { /// diff --git a/src/Avalonia.Controls/TextBox.cs b/src/Avalonia.Controls/TextBox.cs index 73a1ae3335..0fe3ac62e4 100644 --- a/src/Avalonia.Controls/TextBox.cs +++ b/src/Avalonia.Controls/TextBox.cs @@ -13,9 +13,11 @@ using Avalonia.Metadata; using Avalonia.Data; using Avalonia.Layout; using Avalonia.Utilities; +using Avalonia.Controls.Metadata; namespace Avalonia.Controls { + [PseudoClasses(":empty")] public class TextBox : TemplatedControl, UndoRedoHelper.IUndoRedoHost { public static KeyGesture CutGesture { get; } = AvaloniaLocator.Current diff --git a/src/Avalonia.Controls/ToggleSwitch.cs b/src/Avalonia.Controls/ToggleSwitch.cs index c32f2d8102..662a355dac 100644 --- a/src/Avalonia.Controls/ToggleSwitch.cs +++ b/src/Avalonia.Controls/ToggleSwitch.cs @@ -1,4 +1,5 @@ -using Avalonia.Controls.Presenters; +using Avalonia.Controls.Metadata; +using Avalonia.Controls.Presenters; using Avalonia.Controls.Primitives; using Avalonia.Controls.Templates; using Avalonia.LogicalTree; @@ -8,6 +9,7 @@ namespace Avalonia.Controls /// /// A Toggle Switch control. /// + [PseudoClasses(":dragging")] public class ToggleSwitch : ToggleButton { private Panel _knobsPanel; diff --git a/src/Avalonia.Controls/ToolTip.cs b/src/Avalonia.Controls/ToolTip.cs index d56ff5752f..71bd0726d4 100644 --- a/src/Avalonia.Controls/ToolTip.cs +++ b/src/Avalonia.Controls/ToolTip.cs @@ -1,5 +1,6 @@ using System; using System.Reactive.Linq; +using Avalonia.Controls.Metadata; using Avalonia.Controls.Primitives; using Avalonia.VisualTree; @@ -14,6 +15,7 @@ namespace Avalonia.Controls /// To add a tooltip to a control, use the attached property, /// assigning the content that you want displayed. /// + [PseudoClasses(":open")] public class ToolTip : ContentControl { /// diff --git a/src/Avalonia.Controls/TreeViewItem.cs b/src/Avalonia.Controls/TreeViewItem.cs index 4942d4d313..8ce258b546 100644 --- a/src/Avalonia.Controls/TreeViewItem.cs +++ b/src/Avalonia.Controls/TreeViewItem.cs @@ -1,5 +1,6 @@ using System.Linq; using Avalonia.Controls.Generators; +using Avalonia.Controls.Metadata; using Avalonia.Controls.Mixins; using Avalonia.Controls.Primitives; using Avalonia.Controls.Templates; @@ -11,6 +12,7 @@ namespace Avalonia.Controls /// /// An item in a . /// + [PseudoClasses(":pressed", ":selected")] public class TreeViewItem : HeaderedItemsControl, ISelectable { /// diff --git a/src/Avalonia.Input/InputElement.cs b/src/Avalonia.Input/InputElement.cs index 25f2d553d7..9ace7fd92d 100644 --- a/src/Avalonia.Input/InputElement.cs +++ b/src/Avalonia.Input/InputElement.cs @@ -2,6 +2,7 @@ using System; using System.Collections.Generic; using System.Linq; using Avalonia.Controls; +using Avalonia.Controls.Metadata; using Avalonia.Data; using Avalonia.Input.GestureRecognizers; using Avalonia.Interactivity; @@ -12,6 +13,7 @@ namespace Avalonia.Input /// /// Implements input-related functionality for a control. /// + [PseudoClasses(":disabled", ":focus", ":focus-visible", ":pointerover")] public class InputElement : Interactive, IInputElement { /// diff --git a/src/Avalonia.Styling/Controls/Metadata/PseudoClassesAttribute.cs b/src/Avalonia.Styling/Controls/Metadata/PseudoClassesAttribute.cs new file mode 100644 index 0000000000..0060767565 --- /dev/null +++ b/src/Avalonia.Styling/Controls/Metadata/PseudoClassesAttribute.cs @@ -0,0 +1,18 @@ +using System; +using System.Collections.Generic; + +#nullable enable + +namespace Avalonia.Controls.Metadata +{ + [AttributeUsage(AttributeTargets.Class, AllowMultiple = true)] + public sealed class PseudoClassesAttribute : Attribute + { + public PseudoClassesAttribute(params string[] pseudoClasses) + { + PseudoClasses = pseudoClasses; + } + + public IReadOnlyList PseudoClasses { get; } + } +} diff --git a/src/Avalonia.Themes.Fluent/Button.xaml b/src/Avalonia.Themes.Fluent/Button.xaml index e58e8758d2..8522c933ae 100644 --- a/src/Avalonia.Themes.Fluent/Button.xaml +++ b/src/Avalonia.Themes.Fluent/Button.xaml @@ -77,7 +77,7 @@ - - diff --git a/tests/Avalonia.Controls.UnitTests/AutoCompleteBoxTests.cs b/tests/Avalonia.Controls.UnitTests/AutoCompleteBoxTests.cs index 57cea91834..3e78e951e2 100644 --- a/tests/Avalonia.Controls.UnitTests/AutoCompleteBoxTests.cs +++ b/tests/Avalonia.Controls.UnitTests/AutoCompleteBoxTests.cs @@ -363,6 +363,40 @@ namespace Avalonia.Controls.UnitTests }); } + [Fact] + public void Custom_TextSelector() + { + RunTest((control, textbox) => + { + object selectedItem = control.Items.Cast().First(); + string input = "42"; + + control.TextSelector = (text, item) => text + item; + Assert.Equal(control.TextSelector("4", "2"), "42"); + + control.Text = input; + control.SelectedItem = selectedItem; + Assert.Equal(control.Text, control.TextSelector(input, selectedItem.ToString())); + }); + } + + [Fact] + public void Custom_ItemSelector() + { + RunTest((control, textbox) => + { + object selectedItem = control.Items.Cast().First(); + string input = "42"; + + control.ItemSelector = (text, item) => text + item; + Assert.Equal(control.ItemSelector("4", 2), "42"); + + control.Text = input; + control.SelectedItem = selectedItem; + Assert.Equal(control.Text, control.ItemSelector(input, selectedItem)); + }); + } + /// /// Retrieves a defined predicate filter through a new AutoCompleteBox /// control instance.