diff --git a/samples/ControlCatalog.Android/MainActivity.cs b/samples/ControlCatalog.Android/MainActivity.cs index f6fa07dbde..486d14661e 100644 --- a/samples/ControlCatalog.Android/MainActivity.cs +++ b/samples/ControlCatalog.Android/MainActivity.cs @@ -5,7 +5,7 @@ using Avalonia.Android; namespace ControlCatalog.Android { - [Activity(Label = "ControlCatalog.Android", Theme = "@style/MyTheme.Main", Icon = "@drawable/icon", LaunchMode = LaunchMode.SingleTop, ConfigurationChanges = ConfigChanges.Orientation | ConfigChanges.ScreenSize)] + [Activity(Label = "ControlCatalog.Android", Theme = "@style/MyTheme.Main", Icon = "@drawable/icon", LaunchMode = LaunchMode.SingleTop, ConfigurationChanges = ConfigChanges.Orientation | ConfigChanges.ScreenSize | ConfigChanges.UiMode)] public class MainActivity : AvaloniaMainActivity { } diff --git a/samples/ControlCatalog.Android/Resources/values-night/colors.xml b/samples/ControlCatalog.Android/Resources/values-night/colors.xml new file mode 100644 index 0000000000..3d47b6fc58 --- /dev/null +++ b/samples/ControlCatalog.Android/Resources/values-night/colors.xml @@ -0,0 +1,4 @@ + + + #212121 + diff --git a/samples/ControlCatalog/MainView.xaml b/samples/ControlCatalog/MainView.xaml index 9f06525821..7ed2d67379 100644 --- a/samples/ControlCatalog/MainView.xaml +++ b/samples/ControlCatalog/MainView.xaml @@ -147,9 +147,6 @@ - - - diff --git a/samples/ControlCatalog/Pages/ScrollSnapPage.xaml b/samples/ControlCatalog/Pages/ScrollSnapPage.xaml deleted file mode 100644 index fa206f0dff..0000000000 --- a/samples/ControlCatalog/Pages/ScrollSnapPage.xaml +++ /dev/null @@ -1,222 +0,0 @@ - - - Scrollviewer can snap supported content both vertically and horizontally. Snapping occurs from scrolling with touch or pen. - - - - - - - - - - - - - - - - Vertical Snapping - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - Horizontal Snapping - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/samples/ControlCatalog/Pages/ScrollSnapPage.xaml.cs b/samples/ControlCatalog/Pages/ScrollSnapPage.xaml.cs deleted file mode 100644 index 384dc67c66..0000000000 --- a/samples/ControlCatalog/Pages/ScrollSnapPage.xaml.cs +++ /dev/null @@ -1,68 +0,0 @@ -using System.Collections.Generic; -using Avalonia.Controls; -using Avalonia.Controls.Primitives; -using Avalonia.Markup.Xaml; -using MiniMvvm; - -namespace ControlCatalog.Pages -{ - public class ScrollSnapPageViewModel : ViewModelBase - { - private SnapPointsType _snapPointsType; - private SnapPointsAlignment _snapPointsAlignment; - private bool _areSnapPointsRegular; - - public ScrollSnapPageViewModel() - { - - AvailableSnapPointsType = new List() - { - SnapPointsType.None, - SnapPointsType.Mandatory, - SnapPointsType.MandatorySingle - }; - - AvailableSnapPointsAlignment = new List() - { - SnapPointsAlignment.Near, - SnapPointsAlignment.Center, - SnapPointsAlignment.Far, - }; - } - - public bool AreSnapPointsRegular - { - get => _areSnapPointsRegular; - set => this.RaiseAndSetIfChanged(ref _areSnapPointsRegular, value); - } - - public SnapPointsType SnapPointsType - { - get => _snapPointsType; - set => this.RaiseAndSetIfChanged(ref _snapPointsType, value); - } - - public SnapPointsAlignment SnapPointsAlignment - { - get => _snapPointsAlignment; - set => this.RaiseAndSetIfChanged(ref _snapPointsAlignment, value); - } - public List AvailableSnapPointsType { get; } - public List AvailableSnapPointsAlignment { get; } - } - - public class ScrollSnapPage : UserControl - { - public ScrollSnapPage() - { - this.InitializeComponent(); - - DataContext = new ScrollSnapPageViewModel(); - } - - private void InitializeComponent() - { - AvaloniaXamlLoader.Load(this); - } - } -} diff --git a/samples/ControlCatalog/Pages/ScrollViewerPage.xaml b/samples/ControlCatalog/Pages/ScrollViewerPage.xaml index 1a3d61eb85..4af61c3399 100644 --- a/samples/ControlCatalog/Pages/ScrollViewerPage.xaml +++ b/samples/ControlCatalog/Pages/ScrollViewerPage.xaml @@ -3,35 +3,267 @@ xmlns:pages="using:ControlCatalog.Pages" x:Class="ControlCatalog.Pages.ScrollViewerPage" x:DataType="pages:ScrollViewerPageViewModel"> - - Allows for horizontal and vertical content scrolling. Supports snapping on touch and pointer wheel scrolling. - - - - - - - - - - - - - - - + + + + Allows for horizontal and vertical content scrolling. Supports snapping on touch and pointer wheel scrolling. + + + + + + + + + + + + + + + + + + + + + + + + + Scrollviewer can snap supported content both vertically and horizontally. Snapping occurs from scrolling with touch or pen. + + + + + + + - - - - - + + + + + + + + Vertical Snapping + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Horizontal Snapping + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/samples/ControlCatalog/Pages/ScrollViewerPage.xaml.cs b/samples/ControlCatalog/Pages/ScrollViewerPage.xaml.cs index a097f1f951..7082ca1bf6 100644 --- a/samples/ControlCatalog/Pages/ScrollViewerPage.xaml.cs +++ b/samples/ControlCatalog/Pages/ScrollViewerPage.xaml.cs @@ -12,6 +12,9 @@ namespace ControlCatalog.Pages private bool _enableInertia; private ScrollBarVisibility _horizontalScrollVisibility; private ScrollBarVisibility _verticalScrollVisibility; + private SnapPointsType _snapPointsType; + private SnapPointsAlignment _snapPointsAlignment; + private bool _areSnapPointsRegular; public ScrollViewerPageViewModel() { @@ -23,6 +26,20 @@ namespace ControlCatalog.Pages ScrollBarVisibility.Disabled, }; + AvailableSnapPointsType = new List() + { + SnapPointsType.None, + SnapPointsType.Mandatory, + SnapPointsType.MandatorySingle + }; + + AvailableSnapPointsAlignment = new List() + { + SnapPointsAlignment.Near, + SnapPointsAlignment.Center, + SnapPointsAlignment.Far, + }; + HorizontalScrollVisibility = ScrollBarVisibility.Auto; VerticalScrollVisibility = ScrollBarVisibility.Auto; AllowAutoHide = true; @@ -54,6 +71,26 @@ namespace ControlCatalog.Pages } public List AvailableVisibility { get; } + + public bool AreSnapPointsRegular + { + get => _areSnapPointsRegular; + set => this.RaiseAndSetIfChanged(ref _areSnapPointsRegular, value); + } + + public SnapPointsType SnapPointsType + { + get => _snapPointsType; + set => this.RaiseAndSetIfChanged(ref _snapPointsType, value); + } + + public SnapPointsAlignment SnapPointsAlignment + { + get => _snapPointsAlignment; + set => this.RaiseAndSetIfChanged(ref _snapPointsAlignment, value); + } + public List AvailableSnapPointsType { get; } + public List AvailableSnapPointsAlignment { get; } } public class ScrollViewerPage : UserControl diff --git a/samples/VirtualizationDemo/App.axaml b/samples/VirtualizationDemo/App.axaml new file mode 100644 index 0000000000..f5f06ffb6a --- /dev/null +++ b/samples/VirtualizationDemo/App.axaml @@ -0,0 +1,14 @@ + + + + + + + + + + + + diff --git a/samples/VirtualizationDemo/App.axaml.cs b/samples/VirtualizationDemo/App.axaml.cs new file mode 100644 index 0000000000..5ac5c9a92b --- /dev/null +++ b/samples/VirtualizationDemo/App.axaml.cs @@ -0,0 +1,20 @@ +using Avalonia; +using Avalonia.Controls.ApplicationLifetimes; +using Avalonia.Markup.Xaml; + +namespace VirtualizationDemo; + +public partial class App : Application +{ + public override void Initialize() + { + AvaloniaXamlLoader.Load(this); + } + + public override void OnFrameworkInitializationCompleted() + { + if (ApplicationLifetime is IClassicDesktopStyleApplicationLifetime desktop) + desktop.MainWindow = new MainWindow(); + base.OnFrameworkInitializationCompleted(); + } +} diff --git a/samples/VirtualizationDemo/App.xaml b/samples/VirtualizationDemo/App.xaml deleted file mode 100644 index eb5f0e4dca..0000000000 --- a/samples/VirtualizationDemo/App.xaml +++ /dev/null @@ -1,7 +0,0 @@ - - - - - diff --git a/samples/VirtualizationDemo/App.xaml.cs b/samples/VirtualizationDemo/App.xaml.cs deleted file mode 100644 index 81b80c1f40..0000000000 --- a/samples/VirtualizationDemo/App.xaml.cs +++ /dev/null @@ -1,21 +0,0 @@ -using Avalonia; -using Avalonia.Controls.ApplicationLifetimes; -using Avalonia.Markup.Xaml; - -namespace VirtualizationDemo -{ - public class App : Application - { - public override void Initialize() - { - AvaloniaXamlLoader.Load(this); - } - - public override void OnFrameworkInitializationCompleted() - { - if (ApplicationLifetime is IClassicDesktopStyleApplicationLifetime desktop) - desktop.MainWindow = new MainWindow(); - base.OnFrameworkInitializationCompleted(); - } - } -} diff --git a/samples/VirtualizationDemo/Assets/chat.json b/samples/VirtualizationDemo/Assets/chat.json new file mode 100644 index 0000000000..cc628b534a --- /dev/null +++ b/samples/VirtualizationDemo/Assets/chat.json @@ -0,0 +1,190 @@ +{ + "chat": [ + { + "sender": "Alice", + "message": "Hey Bob! How was your weekend?", + "timestamp": "2023-04-01T10:00:00" + }, + { + "sender": "Bob", + "message": "It was great, thanks for asking. I went on a camping trip with some friends. How about you?", + "timestamp": "2023-04-01T10:01:00" + }, + { + "sender": "Alice", + "message": "My weekend was pretty chill. I just stayed home and caught up on some TV shows.", + "timestamp": "2023-04-01T10:03:00" + }, + { + "sender": "Bob", + "message": "That sounds relaxing. What shows did you watch?", + "timestamp": "2023-04-01T10:05:00" + }, + { + "sender": "Alice", + "message": "I watched the new season of 'Stranger Things' and started watching 'Ozark'. Have you seen them?", + "timestamp": "2023-04-01T10:07:00" + }, + { + "sender": "Bob", + "message": "Yeah, I've seen both of those. They're really good! What do you think of them so far?", + "timestamp": "2023-04-01T10:10:00" + }, + { + "sender": "Alice", + "message": "I'm really enjoying 'Stranger Things', but 'Ozark' is a bit darker than I expected. I'm only a few episodes in though, so we'll see how it goes.", + "timestamp": "2023-04-01T10:12:00" + }, + { + "sender": "Bob", + "message": "Yeah, 'Ozark' can be intense at times, but it's really well done. Keep watching, it gets even better.", + "timestamp": "2023-04-01T10:15:00" + }, + { + "sender": "Alice", + "message": "Thanks for the recommendation, I'll definitely keep watching. So, how's work been for you lately?", + "timestamp": "2023-04-01T10:20:00" + }, + { + "sender": "Bob", + "message": "It's been pretty busy, but I'm managing. How about you?", + "timestamp": "2023-04-01T10:22:00" + }, + { + "sender": "Alice", + "message": "Same here, things have been pretty hectic. But it keeps us on our toes, right?", + "timestamp": "2023-04-01T10:25:00" + }, + { + "sender": "Bob", + "message": "Absolutely. Hey, have you heard about the new project we're starting next week?", + "timestamp": "2023-04-01T10:30:00" + }, + { + "sender": "Alice", + "message": "No, I haven't. What's it about?", + "timestamp": "2023-04-01T10:32:00" + }, + { + "sender": "Bob", + "message": "It's a big project for a new client, and it's going to require a lot of extra hours from all of us. But the pay is going to be great,so it's definitely worth the extra effort. I'll fill you in on the details later, but for now, let's just enjoy our coffee break, shall we?", + "timestamp": "2023-04-01T10:35:00" + }, + { + "sender": "Alice", + "message": "Sounds good to me. I could use a break right about now.", + "timestamp": "2023-04-01T10:40:00" + }, + { + "sender": "Bob", + "message": "Me too. So, have you tried the new caf� down the street yet?", + "timestamp": "2023-04-01T10:45:00" + }, + { + "sender": "Alice", + "message": "No, I haven't. Is it any good?", + "timestamp": "2023-04-01T10:47:00" + }, + { + "sender": "Bob", + "message": "It's really good! They have the best croissants I've ever tasted.", + "timestamp": "2023-04-01T10:50:00" + }, + { + "sender": "Alice", + "message": "Hmm, I'll have to try it out sometime. Do they have any vegan options?", + "timestamp": "2023-04-01T10:52:00" + }, + { + "sender": "Bob", + "message": "I'm not sure, but I think they do. You should ask them the next time you go there.", + "timestamp": "2023-04-01T10:55:00" + }, + { + "sender": "Alice", + "message": "Thanks for the suggestion. I'm always looking for good vegan options around here.", + "timestamp": "2023-04-01T11:00:00" + }, + { + "sender": "Bob", + "message": "No problem. So, have you made any plans for the weekend yet?", + "timestamp": "2023-04-01T11:05:00" + }, + { + "sender": "Alice", + "message": "Not yet. I was thinking of maybe going for a hike or something. What about you?", + "timestamp": "2023-04-01T11:07:00" + }, + { + "sender": "Bob", + "message": "I haven't made any plans either. Maybe we could do something together?", + "timestamp": "2023-04-01T11:10:00" + }, + { + "sender": "Alice", + "message": "That sounds like a great idea! Let's plan on it.", + "timestamp": "2023-04-01T11:12:00" + }, + { + "sender": "Bob", + "message": "Awesome. I'll check out some hiking trails and let you know which ones look good.", + "timestamp": "2023-04-01T11:15:00" + }, + { + "sender": "Alice", + "message": "Sounds good. I can't wait!", + "timestamp": "2023-04-01T11:20:00" + }, + { + "sender": "John", + "message": "Hey Lisa, how was your day?", + "timestamp": "2023-04-01T18:00:00" + }, + { + "sender": "Lisa", + "message": "It was good, thanks for asking. How about you?", + "timestamp": "2023-04-01T18:05:00" + }, + { + "sender": "John", + "message": "Eh, it was alright. Work was pretty busy, but nothing too crazy.", + "timestamp": "2023-04-01T18:10:00" + }, + { + "sender": "Lisa", + "message": "Yeah, I know what you mean. My boss has been on my case lately about meeting our deadlines.", + "timestamp": "2023-04-01T18:15:00" + }, + { + "sender": "John", + "message": "That sucks. Are you feeling stressed out?", + "timestamp": "2023-04-01T18:20:00" + }, + { + "sender": "Lisa", + "message": "A little bit, yeah. But I'm trying to stay positive and focus on getting my work done.", + "timestamp": "2023-04-01T18:25:00" + }, + { + "sender": "John", + "message": "That's a good attitude to have. Have you tried doing some meditation or other relaxation techniques?", + "timestamp": "2023-04-01T18:30:00" + }, + { + "sender": "Lisa", + "message": "I haven't, but I've been thinking about it. Do you have any suggestions?", + "timestamp": "2023-04-01T18:35:00" + }, + { + "sender": "John", + "message": "Sure, I could send you some links to guided meditations that I've found helpful. And there are also some great apps out there that can help you with relaxation.", + "timestamp": "2023-04-01T18:40:00" + }, + { + "sender": "Lisa", + "message": "That would be awesome, thanks so much!", + "timestamp": "2023-04-01T18:45:00" + } + ] +} + diff --git a/samples/VirtualizationDemo/MainWindow.axaml b/samples/VirtualizationDemo/MainWindow.axaml new file mode 100644 index 0000000000..04e75450bf --- /dev/null +++ b/samples/VirtualizationDemo/MainWindow.axaml @@ -0,0 +1,20 @@ + + + + + + + + + + + + + diff --git a/samples/VirtualizationDemo/MainWindow.axaml.cs b/samples/VirtualizationDemo/MainWindow.axaml.cs new file mode 100644 index 0000000000..533dc00aa1 --- /dev/null +++ b/samples/VirtualizationDemo/MainWindow.axaml.cs @@ -0,0 +1,15 @@ +using Avalonia; +using Avalonia.Controls; +using VirtualizationDemo.ViewModels; + +namespace VirtualizationDemo; + +public partial class MainWindow : Window +{ + public MainWindow() + { + InitializeComponent(); + this.AttachDevTools(); + DataContext = new MainWindowViewModel(); + } +} diff --git a/samples/VirtualizationDemo/MainWindow.xaml b/samples/VirtualizationDemo/MainWindow.xaml deleted file mode 100644 index 3aee63c246..0000000000 --- a/samples/VirtualizationDemo/MainWindow.xaml +++ /dev/null @@ -1,64 +0,0 @@ - - - - - - - - - Horiz. ScrollBar - - Vert. ScrollBar - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/samples/VirtualizationDemo/MainWindow.xaml.cs b/samples/VirtualizationDemo/MainWindow.xaml.cs deleted file mode 100644 index cea200dcec..0000000000 --- a/samples/VirtualizationDemo/MainWindow.xaml.cs +++ /dev/null @@ -1,22 +0,0 @@ -using Avalonia; -using Avalonia.Controls; -using Avalonia.Markup.Xaml; -using VirtualizationDemo.ViewModels; - -namespace VirtualizationDemo -{ - public class MainWindow : Window - { - public MainWindow() - { - this.InitializeComponent(); - this.AttachDevTools(); - DataContext = new MainWindowViewModel(); - } - - private void InitializeComponent() - { - AvaloniaXamlLoader.Load(this); - } - } -} diff --git a/samples/VirtualizationDemo/Models/Chat.cs b/samples/VirtualizationDemo/Models/Chat.cs new file mode 100644 index 0000000000..813e8650f5 --- /dev/null +++ b/samples/VirtualizationDemo/Models/Chat.cs @@ -0,0 +1,23 @@ +using System; +using System.IO; +using System.Text.Json; + +namespace VirtualizationDemo.Models; + +public class ChatFile +{ + public ChatMessage[]? Chat { get; set; } + + public static ChatFile Load(string path) + { + var options = new JsonSerializerOptions + { + PropertyNameCaseInsensitive = true + }; + + using var s = File.OpenRead(path); + return JsonSerializer.Deserialize(s, options)!; + } +} + +public record ChatMessage(string Sender, string Message, DateTimeOffset Timestamp); diff --git a/samples/VirtualizationDemo/Program.cs b/samples/VirtualizationDemo/Program.cs index febda46450..87212b6daa 100644 --- a/samples/VirtualizationDemo/Program.cs +++ b/samples/VirtualizationDemo/Program.cs @@ -1,15 +1,14 @@ using Avalonia; -namespace VirtualizationDemo +namespace VirtualizationDemo; + +class Program { - class Program - { - public static AppBuilder BuildAvaloniaApp() - => AppBuilder.Configure() - .UsePlatformDetect() - .LogToTrace(); + public static AppBuilder BuildAvaloniaApp() + => AppBuilder.Configure() + .UsePlatformDetect() + .LogToTrace(); - public static int Main(string[] args) - => BuildAvaloniaApp().StartWithClassicDesktopLifetime(args); - } + public static int Main(string[] args) + => BuildAvaloniaApp().StartWithClassicDesktopLifetime(args); } diff --git a/samples/VirtualizationDemo/ViewModels/ChatPageViewModel.cs b/samples/VirtualizationDemo/ViewModels/ChatPageViewModel.cs new file mode 100644 index 0000000000..c0abe62bd5 --- /dev/null +++ b/samples/VirtualizationDemo/ViewModels/ChatPageViewModel.cs @@ -0,0 +1,17 @@ +using System; +using System.Collections.ObjectModel; +using System.IO; +using VirtualizationDemo.Models; + +namespace VirtualizationDemo.ViewModels; + +public class ChatPageViewModel +{ + public ChatPageViewModel() + { + var chat = ChatFile.Load(Path.Combine("Assets", "chat.json")); + Messages = new(chat.Chat ?? Array.Empty()); + } + + public ObservableCollection Messages { get; } +} diff --git a/samples/VirtualizationDemo/ViewModels/ExpanderItemViewModel.cs b/samples/VirtualizationDemo/ViewModels/ExpanderItemViewModel.cs new file mode 100644 index 0000000000..a17fc2d303 --- /dev/null +++ b/samples/VirtualizationDemo/ViewModels/ExpanderItemViewModel.cs @@ -0,0 +1,21 @@ +using MiniMvvm; + +namespace VirtualizationDemo.ViewModels; + +public class ExpanderItemViewModel : ViewModelBase +{ + private string? _header; + private bool _isExpanded; + + public string? Header + { + get => _header; + set => RaiseAndSetIfChanged(ref _header, value); + } + + public bool IsExpanded + { + get => _isExpanded; + set => RaiseAndSetIfChanged(ref _isExpanded, value); + } +} diff --git a/samples/VirtualizationDemo/ViewModels/ExpanderPageViewModel.cs b/samples/VirtualizationDemo/ViewModels/ExpanderPageViewModel.cs new file mode 100644 index 0000000000..f2807a803b --- /dev/null +++ b/samples/VirtualizationDemo/ViewModels/ExpanderPageViewModel.cs @@ -0,0 +1,17 @@ +using System.Collections.ObjectModel; +using System.Linq; + +namespace VirtualizationDemo.ViewModels; + +internal class ExpanderPageViewModel +{ + public ExpanderPageViewModel() + { + Items = new(Enumerable.Range(0, 100).Select(x => new ExpanderItemViewModel + { + Header = $"Item {x}", + })); + } + + public ObservableCollection Items { get; set; } +} diff --git a/samples/VirtualizationDemo/ViewModels/ItemViewModel.cs b/samples/VirtualizationDemo/ViewModels/ItemViewModel.cs deleted file mode 100644 index 9ba505ffe5..0000000000 --- a/samples/VirtualizationDemo/ViewModels/ItemViewModel.cs +++ /dev/null @@ -1,26 +0,0 @@ -using System; -using MiniMvvm; - -namespace VirtualizationDemo.ViewModels -{ - internal class ItemViewModel : ViewModelBase - { - private string _prefix; - private int _index; - private double _height = double.NaN; - - public ItemViewModel(int index, string prefix = "Item") - { - _prefix = prefix; - _index = index; - } - - public string Header => $"{_prefix} {_index}"; - - public double Height - { - get => _height; - set => this.RaiseAndSetIfChanged(ref _height, value); - } - } -} diff --git a/samples/VirtualizationDemo/ViewModels/MainWindowViewModel.cs b/samples/VirtualizationDemo/ViewModels/MainWindowViewModel.cs index 96dbbc1a83..478e40187e 100644 --- a/samples/VirtualizationDemo/ViewModels/MainWindowViewModel.cs +++ b/samples/VirtualizationDemo/ViewModels/MainWindowViewModel.cs @@ -1,160 +1,10 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Reactive; -using Avalonia.Collections; -using Avalonia.Controls; -using Avalonia.Controls.Primitives; -using Avalonia.Layout; -using Avalonia.Controls.Selection; -using MiniMvvm; +using MiniMvvm; -namespace VirtualizationDemo.ViewModels -{ - internal class MainWindowViewModel : ViewModelBase - { - private int _itemCount = 200; - private string _newItemString = "New Item"; - private int _newItemIndex; - private AvaloniaList _items; - private string _prefix = "Item"; - private ScrollBarVisibility _horizontalScrollBarVisibility = ScrollBarVisibility.Auto; - private ScrollBarVisibility _verticalScrollBarVisibility = ScrollBarVisibility.Auto; - private Orientation _orientation = Orientation.Vertical; - - public MainWindowViewModel() - { - this.WhenAnyValue(x => x.ItemCount).Subscribe(ResizeItems); - RecreateCommand = MiniCommand.Create(() => Recreate()); - - AddItemCommand = MiniCommand.Create(() => AddItem()); - - RemoveItemCommand = MiniCommand.Create(() => Remove()); - - SelectFirstCommand = MiniCommand.Create(() => SelectItem(0)); - - SelectLastCommand = MiniCommand.Create(() => SelectItem(Items.Count - 1)); - } - - public string NewItemString - { - get { return _newItemString; } - set { this.RaiseAndSetIfChanged(ref _newItemString, value); } - } - - public int ItemCount - { - get { return _itemCount; } - set { this.RaiseAndSetIfChanged(ref _itemCount, value); } - } - - public SelectionModel Selection { get; } = new SelectionModel(); - - public AvaloniaList Items - { - get { return _items; } - private set { this.RaiseAndSetIfChanged(ref _items, value); } - } - - public Orientation Orientation - { - get { return _orientation; } - set { this.RaiseAndSetIfChanged(ref _orientation, value); } - } - - public IEnumerable Orientations => - Enum.GetValues(typeof(Orientation)).Cast(); - - public ScrollBarVisibility HorizontalScrollBarVisibility - { - get { return _horizontalScrollBarVisibility; } - set { this.RaiseAndSetIfChanged(ref _horizontalScrollBarVisibility, value); } - } +namespace VirtualizationDemo.ViewModels; - public ScrollBarVisibility VerticalScrollBarVisibility - { - get { return _verticalScrollBarVisibility; } - set { this.RaiseAndSetIfChanged(ref _verticalScrollBarVisibility, value); } - } - - public IEnumerable ScrollBarVisibilities => - Enum.GetValues(typeof(ScrollBarVisibility)).Cast(); - - public MiniCommand AddItemCommand { get; private set; } - public MiniCommand RecreateCommand { get; private set; } - public MiniCommand RemoveItemCommand { get; private set; } - public MiniCommand SelectFirstCommand { get; private set; } - public MiniCommand SelectLastCommand { get; private set; } - - public void RandomizeSize() - { - var random = new Random(); - - foreach (var i in Items) - { - i.Height = random.Next(240) + 10; - } - } - - public void ResetSize() - { - foreach (var i in Items) - { - i.Height = double.NaN; - } - } - - private void ResizeItems(int count) - { - if (Items == null) - { - var items = Enumerable.Range(0, count) - .Select(x => new ItemViewModel(x)); - Items = new AvaloniaList(items); - } - else if (count > Items.Count) - { - var items = Enumerable.Range(Items.Count, count - Items.Count) - .Select(x => new ItemViewModel(x)); - Items.AddRange(items); - } - else if (count < Items.Count) - { - Items.RemoveRange(count, Items.Count - count); - } - } - - private void AddItem() - { - var index = Items.Count; - - if (Selection.SelectedItems.Count > 0) - { - index = Selection.SelectedIndex; - } - - Items.Insert(index, new ItemViewModel(_newItemIndex++, NewItemString)); - } - - private void Remove() - { - if (Selection.SelectedItems.Count > 0) - { - Items.RemoveAll(Selection.SelectedItems.ToList()); - } - } - - private void Recreate() - { - _prefix = _prefix == "Item" ? "Recreated" : "Item"; - var items = Enumerable.Range(0, _itemCount) - .Select(x => new ItemViewModel(x, _prefix)); - Items = new AvaloniaList(items); - } - - private void SelectItem(int index) - { - Selection.SelectedIndex = index; - } - } +internal class MainWindowViewModel : ViewModelBase +{ + public PlaygroundPageViewModel Playground { get; } = new(); + public ChatPageViewModel Chat { get; } = new(); + public ExpanderPageViewModel Expanders { get; } = new(); } diff --git a/samples/VirtualizationDemo/ViewModels/PlaygroundItemViewModel.cs b/samples/VirtualizationDemo/ViewModels/PlaygroundItemViewModel.cs new file mode 100644 index 0000000000..584ef4600b --- /dev/null +++ b/samples/VirtualizationDemo/ViewModels/PlaygroundItemViewModel.cs @@ -0,0 +1,17 @@ +using MiniMvvm; + +namespace VirtualizationDemo.ViewModels; + +public class PlaygroundItemViewModel : ViewModelBase +{ + private string? _header; + + public PlaygroundItemViewModel(int index) => Header = $"Item {index}"; + public PlaygroundItemViewModel(string? header) => Header = header; + + public string? Header + { + get => _header; + set => RaiseAndSetIfChanged(ref _header, value); + } +} diff --git a/samples/VirtualizationDemo/ViewModels/PlaygroundPageViewModel.cs b/samples/VirtualizationDemo/ViewModels/PlaygroundPageViewModel.cs new file mode 100644 index 0000000000..98ab91b0a6 --- /dev/null +++ b/samples/VirtualizationDemo/ViewModels/PlaygroundPageViewModel.cs @@ -0,0 +1,95 @@ +using System; +using System.Collections.ObjectModel; +using System.Linq; +using Avalonia; +using Avalonia.Controls; +using Avalonia.Controls.Selection; +using MiniMvvm; + +namespace VirtualizationDemo.ViewModels; + +public class PlaygroundPageViewModel : ViewModelBase +{ + private SelectionMode _selectionMode = SelectionMode.Multiple; + private int _scrollToIndex = 500; + private string? _newItemHeader = "New Item 1"; + + public PlaygroundPageViewModel() + { + Items = new(Enumerable.Range(0, 1000).Select(x => new PlaygroundItemViewModel(x))); + Selection = new(); + } + + public ObservableCollection Items { get; } + + public bool Multiple + { + get => _selectionMode.HasAnyFlag(SelectionMode.Multiple); + set => SetSelectionMode(SelectionMode.Multiple, value); + } + + public bool Toggle + { + get => _selectionMode.HasAnyFlag(SelectionMode.Toggle); + set => SetSelectionMode(SelectionMode.Toggle, value); + } + + public bool AlwaysSelected + { + get => _selectionMode.HasAnyFlag(SelectionMode.AlwaysSelected); + set => SetSelectionMode(SelectionMode.AlwaysSelected, value); + } + + public SelectionModel Selection { get; } + + public SelectionMode SelectionMode + { + get => _selectionMode; + set => RaiseAndSetIfChanged(ref _selectionMode, value); + } + + public int ScrollToIndex + { + get => _scrollToIndex; + set => RaiseAndSetIfChanged(ref _scrollToIndex, value); + } + + public string? NewItemHeader + { + get => _newItemHeader; + set => RaiseAndSetIfChanged(ref _newItemHeader, value); + } + + public void ExecuteScrollToIndex() + { + Selection.Select(ScrollToIndex); + } + + public void RandomizeScrollToIndex() + { + var rnd = new Random(); + ScrollToIndex = rnd.Next(Items.Count); + } + + public void AddAtSelectedIndex() + { + if (Selection.SelectedIndex == -1) + return; + Items.Insert(Selection.SelectedIndex, new(NewItemHeader)); + } + + public void DeleteSelectedItem() + { + var count = Selection.Count; + for (var i = count - 1; i >= 0; i--) + Items.RemoveAt(Selection.SelectedIndexes[i]); + } + + private void SetSelectionMode(SelectionMode mode, bool value) + { + if (value) + SelectionMode |= mode; + else + SelectionMode &= ~mode; + } +} diff --git a/samples/VirtualizationDemo/Views/ChatPageView.axaml b/samples/VirtualizationDemo/Views/ChatPageView.axaml new file mode 100644 index 0000000000..fc182f15ae --- /dev/null +++ b/samples/VirtualizationDemo/Views/ChatPageView.axaml @@ -0,0 +1,39 @@ + + + + + + + + + + + + + + + + + + + + diff --git a/samples/VirtualizationDemo/Views/ChatPageView.axaml.cs b/samples/VirtualizationDemo/Views/ChatPageView.axaml.cs new file mode 100644 index 0000000000..b5c90db69c --- /dev/null +++ b/samples/VirtualizationDemo/Views/ChatPageView.axaml.cs @@ -0,0 +1,11 @@ +using Avalonia.Controls; + +namespace VirtualizationDemo.Views; + +public partial class ChatPageView : UserControl +{ + public ChatPageView() + { + InitializeComponent(); + } +} diff --git a/samples/VirtualizationDemo/Views/ExpanderPageView.axaml b/samples/VirtualizationDemo/Views/ExpanderPageView.axaml new file mode 100644 index 0000000000..972d885229 --- /dev/null +++ b/samples/VirtualizationDemo/Views/ExpanderPageView.axaml @@ -0,0 +1,18 @@ + + + + + + + + + + + diff --git a/samples/VirtualizationDemo/Views/ExpanderPageView.axaml.cs b/samples/VirtualizationDemo/Views/ExpanderPageView.axaml.cs new file mode 100644 index 0000000000..df3689cf24 --- /dev/null +++ b/samples/VirtualizationDemo/Views/ExpanderPageView.axaml.cs @@ -0,0 +1,13 @@ +using Avalonia; +using Avalonia.Controls; +using Avalonia.Markup.Xaml; + +namespace VirtualizationDemo.Views; + +public partial class ExpanderPageView : UserControl +{ + public ExpanderPageView() + { + InitializeComponent(); + } +} \ No newline at end of file diff --git a/samples/VirtualizationDemo/Views/PlaygroundPageView.axaml b/samples/VirtualizationDemo/Views/PlaygroundPageView.axaml new file mode 100644 index 0000000000..52bc6fd27a --- /dev/null +++ b/samples/VirtualizationDemo/Views/PlaygroundPageView.axaml @@ -0,0 +1,66 @@ + + + + + + + + Multiple + Toggle + AlwaysSelected + AutoScrollToSelectedItem + WrapSelection + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/samples/VirtualizationDemo/Views/PlaygroundPageView.axaml.cs b/samples/VirtualizationDemo/Views/PlaygroundPageView.axaml.cs new file mode 100644 index 0000000000..5282475778 --- /dev/null +++ b/samples/VirtualizationDemo/Views/PlaygroundPageView.axaml.cs @@ -0,0 +1,44 @@ +using System; +using System.Linq; +using System.Threading; +using Avalonia; +using Avalonia.Controls; +using Avalonia.Markup.Xaml; +using Avalonia.Threading; + +namespace VirtualizationDemo.Views; + +public partial class PlaygroundPageView : UserControl +{ + private DispatcherTimer _timer; + + public PlaygroundPageView() + { + InitializeComponent(); + + _timer = new DispatcherTimer + { + Interval = TimeSpan.FromMilliseconds(500), + }; + + _timer.Tick += TimerTick; + } + + protected override void OnAttachedToVisualTree(VisualTreeAttachmentEventArgs e) + { + base.OnAttachedToVisualTree(e); + _timer.Start(); + } + + protected override void OnDetachedFromVisualTree(VisualTreeAttachmentEventArgs e) + { + base.OnDetachedFromVisualTree(e); + _timer.Stop(); + } + + private void TimerTick(object? sender, EventArgs e) + { + var message = $"Realized {list.GetRealizedContainers().Count()} of {list.ItemsPanelRoot?.Children.Count}"; + itemCount.Text = message; + } +} diff --git a/samples/VirtualizationDemo/VirtualizationDemo.csproj b/samples/VirtualizationDemo/VirtualizationDemo.csproj index 81b30c6cbe..3ac7aab589 100644 --- a/samples/VirtualizationDemo/VirtualizationDemo.csproj +++ b/samples/VirtualizationDemo/VirtualizationDemo.csproj @@ -1,19 +1,24 @@  - Exe + WinExe net6.0 + true + + + - - + + + + + PreserveNewest + - - - - - + + diff --git a/src/Android/Avalonia.Android/AvaloniaMainActivity.cs b/src/Android/Avalonia.Android/AvaloniaMainActivity.cs index eb4b6bf6a0..b2cd150933 100644 --- a/src/Android/Avalonia.Android/AvaloniaMainActivity.cs +++ b/src/Android/Avalonia.Android/AvaloniaMainActivity.cs @@ -32,10 +32,6 @@ namespace Avalonia.Android { lifetime.View = View; } - - Window?.ClearFlags(WindowManagerFlags.TranslucentStatus); - Window?.AddFlags(WindowManagerFlags.DrawsSystemBarBackgrounds); - base.OnCreate(savedInstanceState); SetContentView(View); diff --git a/src/Android/Avalonia.Android/Platform/AndroidInsetsManager.cs b/src/Android/Avalonia.Android/Platform/AndroidInsetsManager.cs index 35d1b06e6a..251a177432 100644 --- a/src/Android/Avalonia.Android/Platform/AndroidInsetsManager.cs +++ b/src/Android/Avalonia.Android/Platform/AndroidInsetsManager.cs @@ -2,11 +2,10 @@ using System.Collections.Generic; using Android.OS; using Android.Views; -using AndroidX.AppCompat.App; using AndroidX.Core.View; using Avalonia.Android.Platform.SkiaPlatform; using Avalonia.Controls.Platform; -using static Avalonia.Controls.Platform.IInsetsManager; +using Avalonia.Media; namespace Avalonia.Android.Platform { @@ -20,6 +19,7 @@ namespace Avalonia.Android.Platform private bool? _systemUiVisibility; private SystemBarTheme? _statusBarTheme; private bool? _isDefaultSystemBarLightTheme; + private Color? _systemBarColor; public event EventHandler SafeAreaChanged; @@ -36,6 +36,16 @@ namespace Avalonia.Android.Platform } WindowCompat.SetDecorFitsSystemWindows(_activity.Window, !value); + + if(value) + { + _activity.Window.AddFlags(WindowManagerFlags.TranslucentStatus); + _activity.Window.AddFlags(WindowManagerFlags.TranslucentNavigation); + } + else + { + SystemBarColor = _systemBarColor; + } } } @@ -71,7 +81,7 @@ namespace Avalonia.Android.Platform var renderScaling = _topLevel.RenderScaling; var inset = insets.GetInsets( - (DisplayEdgeToEdge ? + (_displayEdgeToEdge ? WindowInsetsCompat.Type.StatusBars() | WindowInsetsCompat.Type.NavigationBars() | WindowInsetsCompat.Type.DisplayCutout() : 0) | WindowInsetsCompat.Type.Ime()); @@ -81,8 +91,8 @@ namespace Avalonia.Android.Platform return new Thickness(inset.Left / renderScaling, inset.Top / renderScaling, inset.Right / renderScaling, - (imeInset.Bottom > 0 && ((_usesLegacyLayouts && !DisplayEdgeToEdge) || !_usesLegacyLayouts) ? - imeInset.Bottom - navBarInset.Bottom : + (imeInset.Bottom > 0 && ((_usesLegacyLayouts && !_displayEdgeToEdge) || !_usesLegacyLayouts) ? + imeInset.Bottom - (_displayEdgeToEdge ? 0 : navBarInset.Bottom) : inset.Bottom) / renderScaling); } @@ -93,6 +103,7 @@ namespace Avalonia.Android.Platform public WindowInsetsCompat OnApplyWindowInsets(View v, WindowInsetsCompat insets) { NotifySafeAreaChanged(SafeAreaPadding); + insets = ViewCompat.OnApplyWindowInsets(v, insets); return insets; } @@ -146,8 +157,6 @@ namespace Avalonia.Android.Platform compat.AppearanceLightStatusBars = value == Controls.Platform.SystemBarTheme.Light; compat.AppearanceLightNavigationBars = value == Controls.Platform.SystemBarTheme.Light; - - AppCompatDelegate.DefaultNightMode = isDefault ? AppCompatDelegate.ModeNightFollowSystem : compat.AppearanceLightStatusBars ? AppCompatDelegate.ModeNightNo : AppCompatDelegate.ModeNightYes; } } @@ -190,10 +199,36 @@ namespace Avalonia.Android.Platform } } + public Color? SystemBarColor + { + get => _systemBarColor; + set + { + _systemBarColor = value; + + if (_systemBarColor is { } color && !_displayEdgeToEdge && _activity.Window != null) + { + _activity.Window.ClearFlags(WindowManagerFlags.TranslucentStatus); + _activity.Window.ClearFlags(WindowManagerFlags.TranslucentNavigation); + _activity.Window.AddFlags(WindowManagerFlags.DrawsSystemBarBackgrounds); + + var androidColor = global::Android.Graphics.Color.Argb(color.A, color.R, color.G, color.B); + _activity.Window.SetStatusBarColor(androidColor); + + if (Build.VERSION.SdkInt >= BuildVersionCodes.O) + { + // As we can only change the navigation bar's foreground api 26 and newer, we only change the background color if running on those versions + _activity.Window.SetNavigationBarColor(androidColor); + } + } + } + } + internal void ApplyStatusBarState() { IsSystemBarVisible = _systemUiVisibility; SystemBarTheme = _statusBarTheme; + SystemBarColor = _systemBarColor; } private class InsetsAnimationCallback : WindowInsetsAnimationCompat.Callback diff --git a/src/Android/Avalonia.Android/Platform/SkiaPlatform/TopLevelImpl.cs b/src/Android/Avalonia.Android/Platform/SkiaPlatform/TopLevelImpl.cs index 17726e6353..126c488d59 100644 --- a/src/Android/Avalonia.Android/Platform/SkiaPlatform/TopLevelImpl.cs +++ b/src/Android/Avalonia.Android/Platform/SkiaPlatform/TopLevelImpl.cs @@ -9,6 +9,7 @@ using Android.Runtime; using Android.Text; using Android.Views; using Android.Views.InputMethods; +using AndroidX.AppCompat.App; using Avalonia.Android.Platform.Specific; using Avalonia.Android.Platform.Specific.Helpers; using Avalonia.Android.Platform.Storage; @@ -286,6 +287,8 @@ namespace Avalonia.Android.Platform.SkiaPlatform _ => null, }; } + + AppCompatDelegate.DefaultNightMode = themeVariant == PlatformThemeVariant.Light ? AppCompatDelegate.ModeNightNo : AppCompatDelegate.ModeNightYes; } public AcrylicPlatformCompensationLevels AcrylicCompensationLevels => new AcrylicPlatformCompensationLevels(1, 1, 1); diff --git a/src/Avalonia.Base/AvaloniaObjectExtensions.cs b/src/Avalonia.Base/AvaloniaObjectExtensions.cs index 9fbf680a5c..0c22213d33 100644 --- a/src/Avalonia.Base/AvaloniaObjectExtensions.cs +++ b/src/Avalonia.Base/AvaloniaObjectExtensions.cs @@ -1,6 +1,6 @@ using System; -using Avalonia.Reactive; using Avalonia.Data; +using Avalonia.Reactive; namespace Avalonia { @@ -34,8 +34,8 @@ namespace Avalonia /// public static IObservable GetObservable(this AvaloniaObject o, AvaloniaProperty property) { - return new AvaloniaPropertyObservable( - o ?? throw new ArgumentNullException(nameof(o)), + return new AvaloniaPropertyObservable( + o ?? throw new ArgumentNullException(nameof(o)), property ?? throw new ArgumentNullException(nameof(property))); } @@ -54,11 +54,23 @@ namespace Avalonia /// public static IObservable GetObservable(this AvaloniaObject o, AvaloniaProperty property) { - return new AvaloniaPropertyObservable( + return new AvaloniaPropertyObservable( o ?? throw new ArgumentNullException(nameof(o)), property ?? throw new ArgumentNullException(nameof(property))); } + /// + /// + /// + /// A method which is executed to convert each property value to . + public static IObservable GetObservable(this AvaloniaObject o, AvaloniaProperty property, Func converter) + { + return new AvaloniaPropertyObservable( + o ?? throw new ArgumentNullException(nameof(o)), + property ?? throw new ArgumentNullException(nameof(property)), + converter ?? throw new ArgumentNullException(nameof(converter))); + } + /// /// Gets an observable for an . /// @@ -75,7 +87,7 @@ namespace Avalonia this AvaloniaObject o, AvaloniaProperty property) { - return new AvaloniaPropertyBindingObservable( + return new AvaloniaPropertyBindingObservable( o ?? throw new ArgumentNullException(nameof(o)), property ?? throw new ArgumentNullException(nameof(property))); } @@ -97,12 +109,27 @@ namespace Avalonia this AvaloniaObject o, AvaloniaProperty property) { - return new AvaloniaPropertyBindingObservable( + return new AvaloniaPropertyBindingObservable( o ?? throw new ArgumentNullException(nameof(o)), property ?? throw new ArgumentNullException(nameof(property))); } + /// + /// + /// + /// A method which is executed to convert each property value to . + public static IObservable> GetBindingObservable( + this AvaloniaObject o, + AvaloniaProperty property, + Func converter) + { + return new AvaloniaPropertyBindingObservable( + o ?? throw new ArgumentNullException(nameof(o)), + property ?? throw new ArgumentNullException(nameof(property)), + converter ?? throw new ArgumentNullException(nameof(converter))); + } + /// /// Gets an observable that listens for property changed events for an /// . @@ -338,7 +365,7 @@ namespace Avalonia return InstancedBinding.OneWay(_source); } } - + private class ClassHandlerObserver : IObserver> { private readonly Action> _action; diff --git a/src/Avalonia.Base/AvaloniaProperty.cs b/src/Avalonia.Base/AvaloniaProperty.cs index e98d9f0517..c57131f7b5 100644 --- a/src/Avalonia.Base/AvaloniaProperty.cs +++ b/src/Avalonia.Base/AvaloniaProperty.cs @@ -499,6 +499,12 @@ namespace Avalonia /// The object instance. internal abstract void RouteClearValue(AvaloniaObject o); + /// + /// Routes an untyped CoerceValue call on a property with its default value to a typed call. + /// + /// The object instance. + internal abstract void RouteCoerceDefaultValue(AvaloniaObject o); + /// /// Routes an untyped GetValue call to a typed call. /// diff --git a/src/Avalonia.Base/CombinedGeometry.cs b/src/Avalonia.Base/CombinedGeometry.cs index 8f080d05c7..4b5866519b 100644 --- a/src/Avalonia.Base/CombinedGeometry.cs +++ b/src/Avalonia.Base/CombinedGeometry.cs @@ -152,19 +152,15 @@ namespace Avalonia.Media var g1 = Geometry1; var g2 = Geometry2; - if (g1 is object && g2 is object) + if (g1?.PlatformImpl != null && g2?.PlatformImpl != null) { var factory = AvaloniaLocator.Current.GetRequiredService(); - return factory.CreateCombinedGeometry(GeometryCombineMode, g1, g2); + return factory.CreateCombinedGeometry(GeometryCombineMode, g1.PlatformImpl, g2.PlatformImpl); } - else if (GeometryCombineMode == GeometryCombineMode.Intersect) - return null; - else if (g1 is object) - return g1.PlatformImpl; - else if (g2 is object) - return g2.PlatformImpl; - else + + if (GeometryCombineMode == GeometryCombineMode.Intersect) return null; + return g1?.PlatformImpl ?? g2?.PlatformImpl; } } } diff --git a/src/Avalonia.Base/Controls/ResourceDictionary.cs b/src/Avalonia.Base/Controls/ResourceDictionary.cs index 5123803f6e..231a19baab 100644 --- a/src/Avalonia.Base/Controls/ResourceDictionary.cs +++ b/src/Avalonia.Base/Controls/ResourceDictionary.cs @@ -15,6 +15,7 @@ namespace Avalonia.Controls /// public class ResourceDictionary : IResourceDictionary { + private object? lastDeferredItemKey; private Dictionary? _inner; private IResourceHost? _owner; private AvaloniaList? _mergedDictionaries; @@ -241,12 +242,27 @@ namespace Avalonia.Controls { if (value is DeferredItem deffered) { - _inner[key] = value = deffered.Factory(null) switch + // Avoid simple reentrancy, which could commonly occur on redefining the resource. + if (lastDeferredItemKey == key) { - ITemplateResult t => t.Result, - object v => v, - _ => null, - }; + value = null; + return false; + } + + try + { + lastDeferredItemKey = key; + _inner[key] = value = deffered.Factory(null) switch + { + ITemplateResult t => t.Result, + { } v => v, + _ => null, + }; + } + finally + { + lastDeferredItemKey = null; + } } return true; } diff --git a/src/Avalonia.Base/Data/Core/AvaloniaPropertyAccessorNode.cs b/src/Avalonia.Base/Data/Core/AvaloniaPropertyAccessorNode.cs index 92fc843394..4bf24e901e 100644 --- a/src/Avalonia.Base/Data/Core/AvaloniaPropertyAccessorNode.cs +++ b/src/Avalonia.Base/Data/Core/AvaloniaPropertyAccessorNode.cs @@ -40,7 +40,7 @@ namespace Avalonia.Data.Core { if (reference.TryGetTarget(out var target) && target is AvaloniaObject obj) { - _subscription = new AvaloniaPropertyObservable(obj, _property).Subscribe(ValueChanged); + _subscription = new AvaloniaPropertyObservable(obj, _property).Subscribe(ValueChanged); } else { diff --git a/src/Avalonia.Base/DirectPropertyBase.cs b/src/Avalonia.Base/DirectPropertyBase.cs index 94dfaaab01..7e5a962157 100644 --- a/src/Avalonia.Base/DirectPropertyBase.cs +++ b/src/Avalonia.Base/DirectPropertyBase.cs @@ -117,6 +117,11 @@ namespace Avalonia o.ClearValue(this); } + internal override void RouteCoerceDefaultValue(AvaloniaObject o) + { + // Do nothing. + } + /// internal override object? RouteGetValue(AvaloniaObject o) { diff --git a/src/Avalonia.Base/Input/GestureRecognizers/ScrollGestureRecognizer.cs b/src/Avalonia.Base/Input/GestureRecognizers/ScrollGestureRecognizer.cs index 1ad2f292ca..b510d44e63 100644 --- a/src/Avalonia.Base/Input/GestureRecognizers/ScrollGestureRecognizer.cs +++ b/src/Avalonia.Base/Input/GestureRecognizers/ScrollGestureRecognizer.cs @@ -4,14 +4,17 @@ using Avalonia.Threading; namespace Avalonia.Input.GestureRecognizers { - public class ScrollGestureRecognizer - : StyledElement, // It's not an "element" in any way, shape or form, but TemplateBinding refuse to work otherwise - IGestureRecognizer + public class ScrollGestureRecognizer : AvaloniaObject, IGestureRecognizer { // Pixels per second speed that is considered to be the stop of inertial scroll internal const double InertialScrollSpeedEnd = 5; public const double InertialResistance = 0.15; + private bool _canHorizontallyScroll; + private bool _canVerticallyScroll; + private bool _isScrollInertiaEnabled; + private int _scrollStartDistance = 30; + private bool _scrolling; private Point _trackedRootPoint; private IPointer? _tracking; @@ -28,34 +31,39 @@ namespace Avalonia.Input.GestureRecognizers /// /// Defines the property. /// - public static readonly StyledProperty CanHorizontallyScrollProperty = - AvaloniaProperty.Register(nameof(CanHorizontallyScroll)); + public static readonly DirectProperty CanHorizontallyScrollProperty = + AvaloniaProperty.RegisterDirect(nameof(CanHorizontallyScroll), + o => o.CanHorizontallyScroll, (o, v) => o.CanHorizontallyScroll = v); /// /// Defines the property. /// - public static readonly StyledProperty CanVerticallyScrollProperty = - AvaloniaProperty.Register(nameof(CanVerticallyScroll)); + public static readonly DirectProperty CanVerticallyScrollProperty = + AvaloniaProperty.RegisterDirect(nameof(CanVerticallyScroll), + o => o.CanVerticallyScroll, (o, v) => o.CanVerticallyScroll = v); /// /// Defines the property. /// - public static readonly StyledProperty IsScrollInertiaEnabledProperty = - AvaloniaProperty.Register(nameof(IsScrollInertiaEnabled)); + public static readonly DirectProperty IsScrollInertiaEnabledProperty = + AvaloniaProperty.RegisterDirect(nameof(IsScrollInertiaEnabled), + o => o.IsScrollInertiaEnabled, (o,v) => o.IsScrollInertiaEnabled = v); /// /// Defines the property. /// - public static readonly StyledProperty ScrollStartDistanceProperty = - AvaloniaProperty.Register(nameof(ScrollStartDistance), 30); - + public static readonly DirectProperty ScrollStartDistanceProperty = + AvaloniaProperty.RegisterDirect(nameof(ScrollStartDistance), + o => o.ScrollStartDistance, (o, v) => o.ScrollStartDistance = v, + unsetValue: 30); + /// /// Gets or sets a value indicating whether the content can be scrolled horizontally. /// public bool CanHorizontallyScroll { - get => GetValue(CanHorizontallyScrollProperty); - set => SetValue(CanHorizontallyScrollProperty, value); + get => _canHorizontallyScroll; + set => SetAndRaise(CanHorizontallyScrollProperty, ref _canHorizontallyScroll, value); } /// @@ -63,17 +71,17 @@ namespace Avalonia.Input.GestureRecognizers /// public bool CanVerticallyScroll { - get => GetValue(CanVerticallyScrollProperty); - set => SetValue(CanVerticallyScrollProperty, value); + get => _canVerticallyScroll; + set => SetAndRaise(CanVerticallyScrollProperty, ref _canVerticallyScroll, value); } - + /// /// Gets or sets whether the gesture should include inertia in it's behavior. /// public bool IsScrollInertiaEnabled { - get => GetValue(IsScrollInertiaEnabledProperty); - set => SetValue(IsScrollInertiaEnabledProperty, value); + get => _isScrollInertiaEnabled; + set => SetAndRaise(IsScrollInertiaEnabledProperty, ref _isScrollInertiaEnabled, value); } /// @@ -81,10 +89,9 @@ namespace Avalonia.Input.GestureRecognizers /// public int ScrollStartDistance { - get => GetValue(ScrollStartDistanceProperty); - set => SetValue(ScrollStartDistanceProperty, value); - } - + get => _scrollStartDistance; + set => SetAndRaise(ScrollStartDistanceProperty, ref _scrollStartDistance, value); + } public void Initialize(IInputElement target, IGestureRecognizerActionsDispatcher actions) { diff --git a/src/Avalonia.Base/Layout/LayoutManager.cs b/src/Avalonia.Base/Layout/LayoutManager.cs index c4742bcba4..e16be3fa85 100644 --- a/src/Avalonia.Base/Layout/LayoutManager.cs +++ b/src/Avalonia.Base/Layout/LayoutManager.cs @@ -269,21 +269,25 @@ namespace Avalonia.Layout } } - private void Measure(Layoutable control) + private bool Measure(Layoutable control) { + if (!control.IsVisible || !control.IsAttachedToVisualTree) + return false; + // Controls closest to the visual root need to be arranged first. We don't try to store // ordered invalidation lists, instead we traverse the tree upwards, measuring the // controls closest to the root first. This has been shown by benchmarks to be the // fastest and most memory-efficient algorithm. if (control.VisualParent is Layoutable parent) { - Measure(parent); + if (!Measure(parent)) + return false; } // If the control being measured has IsMeasureValid == true here then its measure was // handed by an ancestor and can be ignored. The measure may have also caused the // control to be removed. - if (!control.IsMeasureValid && control.IsAttachedToVisualTree) + if (!control.IsMeasureValid) { if (control is ILayoutRoot root) { @@ -294,16 +298,22 @@ namespace Avalonia.Layout control.Measure(control.PreviousMeasure.Value); } } + + return true; } - private void Arrange(Layoutable control) + private bool Arrange(Layoutable control) { + if (!control.IsVisible || !control.IsAttachedToVisualTree) + return false; + if (control.VisualParent is Layoutable parent) { - Arrange(parent); + if (!Arrange(parent)) + return false; } - if (!control.IsArrangeValid && control.IsAttachedToVisualTree) + if (control.IsMeasureValid && !control.IsArrangeValid) { if (control is IEmbeddedLayoutRoot embeddedRoot) control.Arrange(new Rect(embeddedRoot.AllocatedSize)); @@ -316,6 +326,8 @@ namespace Avalonia.Layout control.Arrange(control.PreviousArrange.Value); } } + + return true; } private void QueueLayoutPass() diff --git a/src/Avalonia.Base/Media/DrawingContext.cs b/src/Avalonia.Base/Media/DrawingContext.cs index f2106f2f86..18d6968168 100644 --- a/src/Avalonia.Base/Media/DrawingContext.cs +++ b/src/Avalonia.Base/Media/DrawingContext.cs @@ -132,7 +132,7 @@ namespace Avalonia.Media double radiusX = 0, double radiusY = 0, BoxShadows boxShadows = default) { - if (brush == null && !PenIsVisible(pen)) + if (brush == null && !PenIsVisible(pen) && boxShadows.Count == 0) return; if (!MathUtilities.IsZero(radiusX)) { @@ -160,7 +160,7 @@ namespace Avalonia.Media /// public void DrawRectangle(IBrush? brush, IPen? pen, RoundedRect rrect, BoxShadows boxShadows = default) { - if (brush == null && !PenIsVisible(pen)) + if (brush == null && !PenIsVisible(pen) && boxShadows.Count == 0) return; DrawRectangleCore(brush, pen, rrect, boxShadows); } diff --git a/src/Avalonia.Base/Media/FontManager.cs b/src/Avalonia.Base/Media/FontManager.cs index 2e8d8e415d..4425147098 100644 --- a/src/Avalonia.Base/Media/FontManager.cs +++ b/src/Avalonia.Base/Media/FontManager.cs @@ -30,13 +30,15 @@ namespace Avalonia.Media _fontFallbacks = options?.FontFallbacks; - DefaultFontFamilyName = options?.DefaultFamilyName ?? PlatformImpl.GetDefaultFontFamilyName(); + var defaultFontFamilyName = options?.DefaultFamilyName ?? PlatformImpl.GetDefaultFontFamilyName(); - if (string.IsNullOrEmpty(DefaultFontFamilyName)) + if (string.IsNullOrEmpty(defaultFontFamilyName)) { throw new InvalidOperationException("Default font family name can't be null or empty."); } + DefaultFontFamily = new FontFamily(defaultFontFamilyName); + AddFontCollection(new SystemFontCollection(this)); } @@ -65,9 +67,9 @@ namespace Avalonia.Media } /// - /// Gets the system's default font family's name. + /// Gets the system's default font family. /// - public string DefaultFontFamilyName + public FontFamily DefaultFontFamily { get; } @@ -93,6 +95,11 @@ namespace Avalonia.Media var fontFamily = typeface.FontFamily; + if(typeface.FontFamily.Name == FontFamily.DefaultFontFamilyName) + { + return TryGetGlyphTypeface(new Typeface(DefaultFontFamily, typeface.Style, typeface.Weight, typeface.Stretch), out glyphTypeface); + } + if (fontFamily.Key is FontFamilyKey key) { var source = key.Source; @@ -131,15 +138,21 @@ namespace Avalonia.Media } } - foreach (var familyName in fontFamily.FamilyNames) + for (var i = 0; i < fontFamily.FamilyNames.Count; i++) { + var familyName = fontFamily.FamilyNames[i]; + if (SystemFonts.TryGetGlyphTypeface(familyName, typeface.Style, typeface.Weight, typeface.Stretch, out glyphTypeface)) { - return true; + if (!fontFamily.FamilyNames.HasFallbacks || glyphTypeface.FamilyName != DefaultFontFamily.Name) + { + return true; + } } } - return TryGetGlyphTypeface(new Typeface(DefaultFontFamilyName, typeface.Style, typeface.Weight, typeface.Stretch), out glyphTypeface); + //Nothing was found so use the default + return TryGetGlyphTypeface(new Typeface(DefaultFontFamily, typeface.Style, typeface.Weight, typeface.Stretch), out glyphTypeface); } /// @@ -199,16 +212,37 @@ namespace Avalonia.Media { foreach (var fallback in _fontFallbacks) { - typeface = new Typeface(fallback.FontFamily, fontStyle, fontWeight, fontStretch); + if (fallback.UnicodeRange.IsInRange(codepoint)) + { + typeface = new Typeface(fallback.FontFamily, fontStyle, fontWeight, fontStretch); + + if (TryGetGlyphTypeface(typeface, out var glyphTypeface) && glyphTypeface.TryGetGlyph((uint)codepoint, out _)) + { + return true; + } + } + } + } - if (TryGetGlyphTypeface(typeface, out var glyphTypeface) && glyphTypeface.TryGetGlyph((uint)codepoint, out _)) + //Try to match against fallbacks first + if (fontFamily != null && fontFamily.FamilyNames.HasFallbacks) + { + for (int i = 1; i < fontFamily.FamilyNames.Count; i++) + { + var familyName = fontFamily.FamilyNames[i]; + + foreach (var fontCollection in _fontCollections.Values) { - return true; + if (fontCollection.TryMatchCharacter(codepoint, fontStyle, fontWeight, fontStretch, familyName, culture, out typeface)) + { + return true; + }; } } } - return PlatformImpl.TryMatchCharacter(codepoint, fontStyle, fontWeight, fontStretch, fontFamily, culture, out typeface); + //Try to find a match with the system font manager + return PlatformImpl.TryMatchCharacter(codepoint, fontStyle, fontWeight, fontStretch, culture, out typeface); } } } diff --git a/src/Avalonia.Base/Media/Fonts/EmbeddedFontCollection.cs b/src/Avalonia.Base/Media/Fonts/EmbeddedFontCollection.cs index f2fb490592..4d4751db02 100644 --- a/src/Avalonia.Base/Media/Fonts/EmbeddedFontCollection.cs +++ b/src/Avalonia.Base/Media/Fonts/EmbeddedFontCollection.cs @@ -8,10 +8,8 @@ using Avalonia.Platform; namespace Avalonia.Media.Fonts { - public class EmbeddedFontCollection : IFontCollection + public class EmbeddedFontCollection : FontCollectionBase { - private readonly ConcurrentDictionary> _glyphTypefaceCache = new(); - private readonly List _fontFamilies = new List(1); private readonly Uri _key; @@ -25,13 +23,13 @@ namespace Avalonia.Media.Fonts _source = source; } - public Uri Key => _key; + public override Uri Key => _key; - public FontFamily this[int index] => _fontFamilies[index]; + public override FontFamily this[int index] => _fontFamilies[index]; - public int Count => _fontFamilies.Count; + public override int Count => _fontFamilies.Count; - public void Initialize(IFontManagerImpl fontManager) + public override void Initialize(IFontManagerImpl fontManager) { var assetLoader = AvaloniaLocator.Current.GetRequiredService(); @@ -45,7 +43,7 @@ namespace Avalonia.Media.Fonts { if (!_glyphTypefaceCache.TryGetValue(glyphTypeface.FamilyName, out var glyphTypefaces)) { - glyphTypefaces = new ConcurrentDictionary(); + glyphTypefaces = new ConcurrentDictionary(); if (_glyphTypefaceCache.TryAdd(glyphTypeface.FamilyName, glyphTypefaces)) { @@ -63,27 +61,8 @@ namespace Avalonia.Media.Fonts } } - public void Dispose() - { - foreach (var fontFamily in _fontFamilies) - { - if (_glyphTypefaceCache.TryGetValue(fontFamily.Name, out var glyphTypefaces)) - { - foreach (var glyphTypeface in glyphTypefaces.Values) - { - glyphTypeface.Dispose(); - } - } - } - GC.SuppressFinalize(this); - } - - public IEnumerator GetEnumerator() => _fontFamilies.GetEnumerator(); - - IEnumerator IEnumerable.GetEnumerator() => GetEnumerator(); - - public bool TryGetGlyphTypeface(string familyName, FontStyle style, FontWeight weight, + public override bool TryGetGlyphTypeface(string familyName, FontStyle style, FontWeight weight, FontStretch stretch, [NotNullWhen(true)] out IGlyphTypeface? glyphTypeface) { var key = new FontCollectionKey(style, weight, stretch); @@ -116,175 +95,6 @@ namespace Avalonia.Media.Fonts return false; } - private static bool TryGetNearestMatch( - ConcurrentDictionary glyphTypefaces, - FontCollectionKey key, - [NotNullWhen(true)] out IGlyphTypeface? glyphTypeface) - { - if (glyphTypefaces.TryGetValue(key, out glyphTypeface)) - { - return true; - } - - if (key.Style != FontStyle.Normal) - { - key = key with { Style = FontStyle.Normal }; - } - - if (key.Stretch != FontStretch.Normal) - { - if (TryFindStretchFallback(glyphTypefaces, key, out glyphTypeface)) - { - return true; - } - - if (key.Weight != FontWeight.Normal) - { - if (TryFindStretchFallback(glyphTypefaces, key with { Weight = FontWeight.Normal }, out glyphTypeface)) - { - return true; - } - } - - key = key with { Stretch = FontStretch.Normal }; - } - - if (TryFindWeightFallback(glyphTypefaces, key, out glyphTypeface)) - { - return true; - } - - if (TryFindStretchFallback(glyphTypefaces, key, out glyphTypeface)) - { - return true; - } - - //Take the first glyph typeface we can find. - foreach (var typeface in glyphTypefaces.Values) - { - glyphTypeface = typeface; - - return true; - } - - return false; - } - - private static bool TryFindStretchFallback( - ConcurrentDictionary glyphTypefaces, - FontCollectionKey key, - [NotNullWhen(true)] out IGlyphTypeface? glyphTypeface) - { - glyphTypeface = null; - - var stretch = (int)key.Stretch; - - if (stretch < 5) - { - for (var i = 0; stretch + i < 9; i++) - { - if (glyphTypefaces.TryGetValue(key with { Stretch = (FontStretch)(stretch + i) }, out glyphTypeface)) - { - return true; - } - } - } - else - { - for (var i = 0; stretch - i > 1; i++) - { - if (glyphTypefaces.TryGetValue(key with { Stretch = (FontStretch)(stretch - i) }, out glyphTypeface)) - { - return true; - } - } - } - - return false; - } - - private static bool TryFindWeightFallback( - ConcurrentDictionary glyphTypefaces, - FontCollectionKey key, - [NotNullWhen(true)] out IGlyphTypeface? typeface) - { - typeface = null; - var weight = (int)key.Weight; - - //If the target weight given is between 400 and 500 inclusive - if (weight >= 400 && weight <= 500) - { - //Look for available weights between the target and 500, in ascending order. - for (var i = 0; weight + i <= 500; i += 50) - { - if (glyphTypefaces.TryGetValue(key with { Weight = (FontWeight)(weight + i) }, out typeface)) - { - return true; - } - } - - //If no match is found, look for available weights less than the target, in descending order. - for (var i = 0; weight - i >= 100; i += 50) - { - if (glyphTypefaces.TryGetValue(key with { Weight = (FontWeight)(weight - i) }, out typeface)) - { - return true; - } - } - - //If no match is found, look for available weights greater than 500, in ascending order. - for (var i = 0; weight + i <= 900; i += 50) - { - if (glyphTypefaces.TryGetValue(key with { Weight = (FontWeight)(weight + i) }, out typeface)) - { - return true; - } - } - } - - //If a weight less than 400 is given, look for available weights less than the target, in descending order. - if (weight < 400) - { - for (var i = 0; weight - i >= 100; i += 50) - { - if (glyphTypefaces.TryGetValue(key with { Weight = (FontWeight)(weight - i) }, out typeface)) - { - return true; - } - } - - //If no match is found, look for available weights less than the target, in descending order. - for (var i = 0; weight + i <= 900; i += 50) - { - if (glyphTypefaces.TryGetValue(key with { Weight = (FontWeight)(weight + i) }, out typeface)) - { - return true; - } - } - } - - //If a weight greater than 500 is given, look for available weights greater than the target, in ascending order. - if (weight > 500) - { - for (var i = 0; weight + i <= 900; i += 50) - { - if (glyphTypefaces.TryGetValue(key with { Weight = (FontWeight)(weight + i) }, out typeface)) - { - return true; - } - } - - //If no match is found, look for available weights less than the target, in descending order. - for (var i = 0; weight - i >= 100; i += 50) - { - if (glyphTypefaces.TryGetValue(key with { Weight = (FontWeight)(weight - i) }, out typeface)) - { - return true; - } - } - } - - return false; - } + public override IEnumerator GetEnumerator() => _fontFamilies.GetEnumerator(); } } diff --git a/src/Avalonia.Base/Media/Fonts/FontCollectionBase.cs b/src/Avalonia.Base/Media/Fonts/FontCollectionBase.cs new file mode 100644 index 0000000000..713b3dafcd --- /dev/null +++ b/src/Avalonia.Base/Media/Fonts/FontCollectionBase.cs @@ -0,0 +1,259 @@ +using System; +using System.Collections; +using System.Collections.Concurrent; +using System.Collections.Generic; +using System.Diagnostics.CodeAnalysis; +using System.Globalization; +using Avalonia.Platform; + +namespace Avalonia.Media.Fonts +{ + public abstract class FontCollectionBase : IFontCollection + { + protected readonly ConcurrentDictionary> _glyphTypefaceCache = new(); + + public abstract Uri Key { get; } + + public abstract int Count { get; } + + public abstract FontFamily this[int index] { get; } + + public abstract bool TryGetGlyphTypeface(string familyName, FontStyle style, FontWeight weight, FontStretch stretch, + [NotNullWhen(true)] out IGlyphTypeface? glyphTypeface); + + public bool TryMatchCharacter(int codepoint, FontStyle style, FontWeight weight, FontStretch stretch, + string? familyName, CultureInfo? culture, out Typeface match) + { + match = default; + + if (string.IsNullOrEmpty(familyName)) + { + foreach (var typefaces in _glyphTypefaceCache.Values) + { + if (TryGetNearestMatch(typefaces, new FontCollectionKey { Style = style, Weight = weight, Stretch = stretch }, out var glyphTypeface)) + { + if (glyphTypeface.TryGetGlyph((uint)codepoint, out _)) + { + match = new Typeface(glyphTypeface.FamilyName, style, weight, stretch); + + return true; + } + } + } + } + else + { + if (TryGetGlyphTypeface(familyName, style, weight, stretch, out var glyphTypeface)) + { + if (glyphTypeface.TryGetGlyph((uint)codepoint, out _)) + { + match = new Typeface(familyName, style, weight, stretch); + + return true; + } + } + } + + return false; + } + + public abstract void Initialize(IFontManagerImpl fontManager); + + public abstract IEnumerator GetEnumerator(); + + void IDisposable.Dispose() + { + foreach (var glyphTypefaces in _glyphTypefaceCache.Values) + { + foreach (var pair in glyphTypefaces) + { + pair.Value?.Dispose(); + } + } + + GC.SuppressFinalize(this); + } + + IEnumerator IEnumerable.GetEnumerator() + { + return GetEnumerator(); + } + + internal static bool TryGetNearestMatch( + ConcurrentDictionary glyphTypefaces, + FontCollectionKey key, + [NotNullWhen(true)] out IGlyphTypeface? glyphTypeface) + { + if (glyphTypefaces.TryGetValue(key, out glyphTypeface) && glyphTypeface != null) + { + return true; + } + + if (key.Style != FontStyle.Normal) + { + key = key with { Style = FontStyle.Normal }; + } + + if (key.Stretch != FontStretch.Normal) + { + if (TryFindStretchFallback(glyphTypefaces, key, out glyphTypeface)) + { + return true; + } + + if (key.Weight != FontWeight.Normal) + { + if (TryFindStretchFallback(glyphTypefaces, key with { Weight = FontWeight.Normal }, out glyphTypeface)) + { + return true; + } + } + + key = key with { Stretch = FontStretch.Normal }; + } + + if (TryFindWeightFallback(glyphTypefaces, key, out glyphTypeface)) + { + return true; + } + + if (TryFindStretchFallback(glyphTypefaces, key, out glyphTypeface)) + { + return true; + } + + //Take the first glyph typeface we can find. + foreach (var typeface in glyphTypefaces.Values) + { + if(typeface != null) + { + glyphTypeface = typeface; + + return true; + } + } + + return false; + } + + internal static bool TryFindStretchFallback( + ConcurrentDictionary glyphTypefaces, + FontCollectionKey key, + [NotNullWhen(true)] out IGlyphTypeface? glyphTypeface) + { + glyphTypeface = null; + + var stretch = (int)key.Stretch; + + if (stretch < 5) + { + for (var i = 0; stretch + i < 9; i++) + { + if (glyphTypefaces.TryGetValue(key with { Stretch = (FontStretch)(stretch + i) }, out glyphTypeface) && glyphTypeface != null) + { + return true; + } + } + } + else + { + for (var i = 0; stretch - i > 1; i++) + { + if (glyphTypefaces.TryGetValue(key with { Stretch = (FontStretch)(stretch - i) }, out glyphTypeface) && glyphTypeface != null) + { + return true; + } + } + } + + return false; + } + + internal static bool TryFindWeightFallback( + ConcurrentDictionary glyphTypefaces, + FontCollectionKey key, + [NotNullWhen(true)] out IGlyphTypeface? glyphTypeface) + { + glyphTypeface = null; + var weight = (int)key.Weight; + + //If the target weight given is between 400 and 500 inclusive + if (weight >= 400 && weight <= 500) + { + //Look for available weights between the target and 500, in ascending order. + for (var i = 0; weight + i <= 500; i += 50) + { + if (glyphTypefaces.TryGetValue(key with { Weight = (FontWeight)(weight + i) }, out glyphTypeface) && glyphTypeface != null) + { + return true; + } + } + + //If no match is found, look for available weights less than the target, in descending order. + for (var i = 0; weight - i >= 100; i += 50) + { + if (glyphTypefaces.TryGetValue(key with { Weight = (FontWeight)(weight - i) }, out glyphTypeface) && glyphTypeface != null) + { + return true; + } + } + + //If no match is found, look for available weights greater than 500, in ascending order. + for (var i = 0; weight + i <= 900; i += 50) + { + if (glyphTypefaces.TryGetValue(key with { Weight = (FontWeight)(weight + i) }, out glyphTypeface) && glyphTypeface != null) + { + return true; + } + } + } + + //If a weight less than 400 is given, look for available weights less than the target, in descending order. + if (weight < 400) + { + for (var i = 0; weight - i >= 100; i += 50) + { + if (glyphTypefaces.TryGetValue(key with { Weight = (FontWeight)(weight - i) }, out glyphTypeface) && glyphTypeface != null) + { + return true; + } + } + + //If no match is found, look for available weights less than the target, in descending order. + for (var i = 0; weight + i <= 900; i += 50) + { + if (glyphTypefaces.TryGetValue(key with { Weight = (FontWeight)(weight + i) }, out glyphTypeface) && glyphTypeface != null) + { + return true; + } + } + } + + //If a weight greater than 500 is given, look for available weights greater than the target, in ascending order. + if (weight > 500) + { + for (var i = 0; weight + i <= 900; i += 50) + { + if (glyphTypefaces.TryGetValue(key with { Weight = (FontWeight)(weight + i) }, out glyphTypeface) && glyphTypeface != null) + { + return true; + } + } + + //If no match is found, look for available weights less than the target, in descending order. + for (var i = 0; weight - i >= 100; i += 50) + { + if (glyphTypefaces.TryGetValue(key with { Weight = (FontWeight)(weight - i) }, out glyphTypeface) && glyphTypeface != null) + { + return true; + } + } + } + + return false; + } + } +} diff --git a/src/Avalonia.Base/Media/Fonts/IFontCollection.cs b/src/Avalonia.Base/Media/Fonts/IFontCollection.cs index 814230bcf3..1a30f168f1 100644 --- a/src/Avalonia.Base/Media/Fonts/IFontCollection.cs +++ b/src/Avalonia.Base/Media/Fonts/IFontCollection.cs @@ -1,6 +1,7 @@ using System; using System.Collections.Generic; using System.Diagnostics.CodeAnalysis; +using System.Globalization; using Avalonia.Platform; namespace Avalonia.Media.Fonts @@ -29,5 +30,21 @@ namespace Avalonia.Media.Fonts /// Returns true if a glyph typface can be found; otherwise, false bool TryGetGlyphTypeface(string familyName, FontStyle style, FontWeight weight, FontStretch stretch, [NotNullWhen(true)] out IGlyphTypeface? glyphTypeface); + + /// + /// Tries to match a specified character to a that supports specified font properties. + /// + /// The codepoint to match against. + /// The font style. + /// The font weight. + /// The font stretch. + /// The family name. This is optional and used for fallback lookup. + /// The culture. + /// The matching . + /// + /// True, if the could match the character to specified parameters, False otherwise. + /// + bool TryMatchCharacter(int codepoint, FontStyle fontStyle, FontWeight fontWeight, + FontStretch fontStretch, string? familyName, CultureInfo? culture, out Typeface typeface); } } diff --git a/src/Avalonia.Base/Media/Fonts/SystemFontCollection.cs b/src/Avalonia.Base/Media/Fonts/SystemFontCollection.cs index fd332c6ebe..2f2948cb3e 100644 --- a/src/Avalonia.Base/Media/Fonts/SystemFontCollection.cs +++ b/src/Avalonia.Base/Media/Fonts/SystemFontCollection.cs @@ -1,5 +1,4 @@ using System; -using System.Collections; using System.Collections.Concurrent; using System.Collections.Generic; using System.Diagnostics.CodeAnalysis; @@ -7,10 +6,8 @@ using Avalonia.Platform; namespace Avalonia.Media.Fonts { - internal class SystemFontCollection : IFontCollection + internal class SystemFontCollection : FontCollectionBase { - private readonly ConcurrentDictionary> _glyphTypefaceCache = new(); - private readonly FontManager _fontManager; private readonly string[] _familyNames; @@ -20,9 +17,9 @@ namespace Avalonia.Media.Fonts _familyNames = fontManager.PlatformImpl.GetInstalledFontFamilyNames(); } - public Uri Key => FontManager.SystemFontsKey; + public override Uri Key => FontManager.SystemFontsKey; - public FontFamily this[int index] + public override FontFamily this[int index] { get { @@ -32,76 +29,41 @@ namespace Avalonia.Media.Fonts } } - public int Count => _familyNames.Length; + public override int Count => _familyNames.Length; - public bool TryGetGlyphTypeface(string familyName, FontStyle style, FontWeight weight, + public override bool TryGetGlyphTypeface(string familyName, FontStyle style, FontWeight weight, FontStretch stretch, [NotNullWhen(true)] out IGlyphTypeface? glyphTypeface) { - if (familyName == FontFamily.DefaultFontFamilyName) - { - familyName = _fontManager.DefaultFontFamilyName; - } + glyphTypeface = null; var key = new FontCollectionKey(style, weight, stretch); - if (_glyphTypefaceCache.TryGetValue(familyName, out var glyphTypefaces)) - { - if (glyphTypefaces.TryGetValue(key, out glyphTypeface)) - { - return true; - } - else - { - if (_fontManager.PlatformImpl.TryCreateGlyphTypeface(familyName, style, weight, stretch, out glyphTypeface) && - glyphTypefaces.TryAdd(key, glyphTypeface)) - { - return true; - } - } - } + var glyphTypefaces = _glyphTypefaceCache.GetOrAdd(familyName, (key) => new ConcurrentDictionary()); - if (_fontManager.PlatformImpl.TryCreateGlyphTypeface(familyName, style, weight, stretch, out glyphTypeface)) + if (!glyphTypefaces.TryGetValue(key, out glyphTypeface)) { - glyphTypefaces = new ConcurrentDictionary(); + _fontManager.PlatformImpl.TryCreateGlyphTypeface(familyName, style, weight, stretch, out glyphTypeface); - if (glyphTypefaces.TryAdd(key, glyphTypeface) && _glyphTypefaceCache.TryAdd(familyName, glyphTypefaces)) + if (!glyphTypefaces.TryAdd(key, glyphTypeface)) { - return true; + return false; } } - return false; + return glyphTypeface != null; } - public void Initialize(IFontManagerImpl fontManager) + public override void Initialize(IFontManagerImpl fontManager) { //We initialize the system font collection during construction. } - IEnumerator IEnumerable.GetEnumerator() - { - return GetEnumerator(); - } - - public IEnumerator GetEnumerator() + public override IEnumerator GetEnumerator() { foreach (var familyName in _familyNames) { yield return new FontFamily(familyName); } } - - void IDisposable.Dispose() - { - foreach (var glyphTypefaces in _glyphTypefaceCache.Values) - { - foreach (var pair in glyphTypefaces) - { - pair.Value.Dispose(); - } - } - - GC.SuppressFinalize(this); - } } } diff --git a/src/Avalonia.Base/Media/GeometryGroup.cs b/src/Avalonia.Base/Media/GeometryGroup.cs index 0326e606f4..3e61413919 100644 --- a/src/Avalonia.Base/Media/GeometryGroup.cs +++ b/src/Avalonia.Base/Media/GeometryGroup.cs @@ -78,7 +78,10 @@ namespace Avalonia.Media { var factory = AvaloniaLocator.Current.GetRequiredService(); - return factory.CreateGeometryGroup(FillRule, _children); + var children = new IGeometryImpl?[_children.Count]; + for (var c = 0; c < _children.Count; c++) + children[c] = _children[c].PlatformImpl; + return factory.CreateGeometryGroup(FillRule, children!); } return null; diff --git a/src/Avalonia.Base/Media/TextFormatting/TextFormatterImpl.cs b/src/Avalonia.Base/Media/TextFormatting/TextFormatterImpl.cs index a40cbf95ad..a609800fb8 100644 --- a/src/Avalonia.Base/Media/TextFormatting/TextFormatterImpl.cs +++ b/src/Avalonia.Base/Media/TextFormatting/TextFormatterImpl.cs @@ -57,21 +57,21 @@ namespace Avalonia.Media.TextFormatting switch (paragraphProperties.TextWrapping) { case TextWrapping.NoWrap: - { - var textLine = new TextLineImpl(shapedTextRuns.ToArray(), firstTextSourceIndex, - textSourceLength, - paragraphWidth, paragraphProperties, resolvedFlowDirection, nextLineBreak); + { + var textLine = new TextLineImpl(shapedTextRuns.ToArray(), firstTextSourceIndex, + textSourceLength, + paragraphWidth, paragraphProperties, resolvedFlowDirection, nextLineBreak); textLine.FinalizeLine(); - return textLine; - } + return textLine; + } case TextWrapping.WrapWithOverflow: case TextWrapping.Wrap: - { - return PerformTextWrapping(shapedTextRuns, false, firstTextSourceIndex, paragraphWidth, - paragraphProperties, resolvedFlowDirection, nextLineBreak, objectPool); - } + { + return PerformTextWrapping(shapedTextRuns, false, firstTextSourceIndex, paragraphWidth, + paragraphProperties, resolvedFlowDirection, nextLineBreak, objectPool); + } default: throw new ArgumentOutOfRangeException(nameof(paragraphProperties.TextWrapping)); } @@ -568,9 +568,9 @@ namespace Avalonia.Media.TextFormatting return false; } - private static bool TryMeasureLength(IReadOnlyList textRuns, double paragraphWidth, out int measuredLength) + private static int MeasureLength(IReadOnlyList textRuns, double paragraphWidth) { - measuredLength = 0; + var measuredLength = 0; var currentWidth = 0.0; for (var i = 0; i < textRuns.Count; ++i) @@ -583,25 +583,59 @@ namespace Avalonia.Media.TextFormatting { if (shapedTextCharacters.ShapedBuffer.Length > 0) { - var firstCluster = shapedTextCharacters.ShapedBuffer[0].GlyphCluster; - var lastCluster = firstCluster; + var runLength = 0; for (var j = 0; j < shapedTextCharacters.ShapedBuffer.Length; j++) { - var glyphInfo = shapedTextCharacters.ShapedBuffer[j]; + var currentInfo = shapedTextCharacters.ShapedBuffer[j]; + + var clusterWidth = currentInfo.GlyphAdvance; - if (currentWidth + glyphInfo.GlyphAdvance > paragraphWidth) + GlyphInfo nextInfo = default; + + while (j + 1 < shapedTextCharacters.ShapedBuffer.Length) { - measuredLength += Math.Max(0, lastCluster - firstCluster); + nextInfo = shapedTextCharacters.ShapedBuffer[j + 1]; + + if (currentInfo.GlyphCluster == nextInfo.GlyphCluster) + { + clusterWidth += nextInfo.GlyphAdvance; + + j++; + + continue; + } + + break; + } - return measuredLength != 0; + var clusterLength = Math.Max(0, nextInfo.GlyphCluster - currentInfo.GlyphCluster); + + if(clusterLength == 0) + { + clusterLength = currentRun.Length - runLength; + } + + if(clusterLength == 0) + { + clusterLength = shapedTextCharacters.GlyphRun.Metrics.FirstCluster + currentRun.Length - currentInfo.GlyphCluster; + } + + if (currentWidth + clusterWidth > paragraphWidth) + { + if (runLength == 0 && measuredLength == 0) + { + runLength = clusterLength; + } + + return measuredLength + runLength; } - lastCluster = glyphInfo.GlyphCluster; - currentWidth += glyphInfo.GlyphAdvance; + currentWidth += clusterWidth; + runLength += clusterLength; } - measuredLength += currentRun.Length; + measuredLength += runLength; } break; @@ -611,7 +645,7 @@ namespace Avalonia.Media.TextFormatting { if (currentWidth + drawableTextRun.Size.Width >= paragraphWidth) { - return measuredLength != 0; + return measuredLength; } measuredLength += currentRun.Length; @@ -628,7 +662,7 @@ namespace Avalonia.Media.TextFormatting } } - return measuredLength != 0; + return measuredLength; } /// @@ -675,9 +709,11 @@ namespace Avalonia.Media.TextFormatting return CreateEmptyTextLine(firstTextSourceIndex, paragraphWidth, paragraphProperties); } - if (!TryMeasureLength(textRuns, paragraphWidth, out var measuredLength)) + var measuredLength = MeasureLength(textRuns, paragraphWidth); + + if(measuredLength == 0) { - measuredLength = 1; + } var currentLength = 0; @@ -798,6 +834,12 @@ namespace Avalonia.Media.TextFormatting continue; } + //We don't want to surpass the measuredLength with trailing whitespace when we are in a right to left setting. + if(currentPosition > measuredLength && resolvedFlowDirection == FlowDirection.RightToLeft) + { + break; + } + measuredLength = currentPosition; break; diff --git a/src/Avalonia.Base/Media/TextFormatting/TextLineImpl.cs b/src/Avalonia.Base/Media/TextFormatting/TextLineImpl.cs index f5812f71ff..3264d5e88a 100644 --- a/src/Avalonia.Base/Media/TextFormatting/TextLineImpl.cs +++ b/src/Avalonia.Base/Media/TextFormatting/TextLineImpl.cs @@ -701,7 +701,14 @@ namespace Avalonia.Media.TextFormatting if (directionalWidth == 0) { //In case a run only contains a linebreak we don't want to skip it. - if (currentRun is ShapedTextRun shaped && currentRun.Length - shaped.GlyphRun.Metrics.NewLineLength > 0) + if (currentRun is ShapedTextRun shaped) + { + if(currentRun.Length - shaped.GlyphRun.Metrics.NewLineLength > 0) + { + continue; + } + } + else { continue; } @@ -840,7 +847,14 @@ namespace Avalonia.Media.TextFormatting if (directionalWidth == 0) { //In case a run only contains a linebreak we don't want to skip it. - if (currentRun is ShapedTextRun shaped && currentRun.Length - shaped.GlyphRun.Metrics.NewLineLength > 0) + if (currentRun is ShapedTextRun shaped) + { + if (currentRun.Length - shaped.GlyphRun.Metrics.NewLineLength > 0) + { + continue; + } + } + else { continue; } diff --git a/src/Avalonia.Base/Platform/IFontManagerImpl.cs b/src/Avalonia.Base/Platform/IFontManagerImpl.cs index 116f7cd6e2..222e7196bb 100644 --- a/src/Avalonia.Base/Platform/IFontManagerImpl.cs +++ b/src/Avalonia.Base/Platform/IFontManagerImpl.cs @@ -27,15 +27,13 @@ namespace Avalonia.Platform /// The font style. /// The font weight. /// The font stretch. - /// The font family. This is optional and used for fallback lookup. /// The culture. /// The matching typeface. /// /// True, if the could match the character to specified parameters, False otherwise. /// bool TryMatchCharacter(int codepoint, FontStyle fontStyle, - FontWeight fontWeight, FontStretch fontStretch, - FontFamily? fontFamily, CultureInfo? culture, out Typeface typeface); + FontWeight fontWeight, FontStretch fontStretch, CultureInfo? culture, out Typeface typeface); /// /// Tries to get a glyph typeface for specified parameters. diff --git a/src/Avalonia.Base/Platform/IPlatformRenderInterface.cs b/src/Avalonia.Base/Platform/IPlatformRenderInterface.cs index cfc7fac3ea..81fe2c046f 100644 --- a/src/Avalonia.Base/Platform/IPlatformRenderInterface.cs +++ b/src/Avalonia.Base/Platform/IPlatformRenderInterface.cs @@ -48,7 +48,7 @@ namespace Avalonia.Platform /// The fill rule. /// The geometries to group. /// A combined geometry. - IGeometryImpl CreateGeometryGroup(FillRule fillRule, IReadOnlyList children); + IGeometryImpl CreateGeometryGroup(FillRule fillRule, IReadOnlyList children); /// /// Creates a geometry group implementation. @@ -57,7 +57,7 @@ namespace Avalonia.Platform /// The first geometry. /// The second geometry. /// A combined geometry. - IGeometryImpl CreateCombinedGeometry(GeometryCombineMode combineMode, Geometry g1, Geometry g2); + IGeometryImpl CreateCombinedGeometry(GeometryCombineMode combineMode, IGeometryImpl g1, IGeometryImpl g2); /// /// Created a geometry implementation for the glyph run. diff --git a/src/Avalonia.Base/PropertyStore/EffectiveValue.cs b/src/Avalonia.Base/PropertyStore/EffectiveValue.cs index 7e9f9ae9ba..a00a51e694 100644 --- a/src/Avalonia.Base/PropertyStore/EffectiveValue.cs +++ b/src/Avalonia.Base/PropertyStore/EffectiveValue.cs @@ -36,12 +36,23 @@ namespace Avalonia.PropertyStore /// public IValueEntry? BaseValueEntry { get; private set; } + /// + /// Gets a value indicating whether the property has a coercion function. + /// + public bool HasCoercion { get; protected set; } + /// /// Gets a value indicating whether the was overridden by a call to /// . /// public bool IsOverridenCurrentValue { get; set; } + /// + /// Gets a value indicating whether the is the result of the + /// + /// + public bool IsCoercedDefaultValue { get; set; } + /// /// Begins a reevaluation pass on the effective value. /// @@ -63,10 +74,33 @@ namespace Avalonia.PropertyStore /// /// Ends a reevaluation pass on the effective value. /// + /// The associated value store. + /// The property being reevaluated. /// - /// This method unsubscribes from any unused value entries. + /// Handles coercing the default value if necessary. /// - public void EndReevaluation() + public void EndReevaluation(ValueStore owner, AvaloniaProperty property) + { + if (Priority == BindingPriority.Unset && HasCoercion) + CoerceDefaultValueAndRaise(owner, property); + } + + /// + /// Gets a value indicating whether the effective value represents the default value of the + /// property and can be removed. + /// + /// True if the effective value van be removed; otherwise false. + public bool CanRemove() + { + return Priority == BindingPriority.Unset && + !IsOverridenCurrentValue && + !IsCoercedDefaultValue; + } + + /// + /// Unsubscribes from any unused value entries. + /// + public void UnsubscribeIfNecessary() { if (Priority == BindingPriority.Unset) { @@ -130,6 +164,17 @@ namespace Avalonia.PropertyStore /// The property being cleared. public abstract void DisposeAndRaiseUnset(ValueStore owner, AvaloniaProperty property); + /// + /// Coerces the default value, raising + /// where necessary. + /// + /// The associated value store. + /// The property being coerced. + protected abstract void CoerceDefaultValueAndRaise(ValueStore owner, AvaloniaProperty property); + + /// + /// Gets the current effective value as a boxed value. + /// protected abstract object? GetBoxedValue(); protected void UpdateValueEntry(IValueEntry? entry, BindingPriority priority) diff --git a/src/Avalonia.Base/PropertyStore/EffectiveValue`1.cs b/src/Avalonia.Base/PropertyStore/EffectiveValue`1.cs index 330034f51d..b725326855 100644 --- a/src/Avalonia.Base/PropertyStore/EffectiveValue`1.cs +++ b/src/Avalonia.Base/PropertyStore/EffectiveValue`1.cs @@ -3,7 +3,6 @@ using System.Collections.Generic; using System.Diagnostics; using System.Diagnostics.CodeAnalysis; using Avalonia.Data; -using static Avalonia.Rendering.Composition.Animations.PropertySetSnapshot; namespace Avalonia.PropertyStore { @@ -33,19 +32,16 @@ namespace Avalonia.PropertyStore if (_metadata.CoerceValue is { } coerce) { + HasCoercion = true; _uncommon = new() { _coerce = coerce, _uncoercedValue = value, _uncoercedBaseValue = value, }; - - Value = coerce(owner, value); - } - else - { - Value = value; } + + Value = value; } /// @@ -61,7 +57,7 @@ namespace Avalonia.PropertyStore Debug.Assert(priority != BindingPriority.LocalValue); UpdateValueEntry(value, priority); - SetAndRaiseCore(owner, (StyledProperty)value.Property, GetValue(value), priority, false); + SetAndRaiseCore(owner, (StyledProperty)value.Property, GetValue(value), priority); if (priority > BindingPriority.LocalValue && value.GetDataValidationState(out var state, out var error)) @@ -75,7 +71,7 @@ namespace Avalonia.PropertyStore StyledProperty property, T value) { - SetAndRaiseCore(owner, property, value, BindingPriority.LocalValue, false); + SetAndRaiseCore(owner, property, value, BindingPriority.LocalValue); } public void SetCurrentValueAndRaise( @@ -83,8 +79,15 @@ namespace Avalonia.PropertyStore StyledProperty property, T value) { - IsOverridenCurrentValue = true; - SetAndRaiseCore(owner, property, value, Priority, true); + SetAndRaiseCore(owner, property, value, Priority, isOverriddenCurrentValue: true); + } + + public void SetCoercedDefaultValueAndRaise( + ValueStore owner, + StyledProperty property, + T value) + { + SetAndRaiseCore(owner, property, value, Priority, isCoercedDefaultValue: true); } public bool TryGetBaseValue([MaybeNullWhen(false)] out T value) @@ -117,7 +120,7 @@ namespace Avalonia.PropertyStore Debug.Assert(Priority != BindingPriority.Animation); Debug.Assert(BasePriority != BindingPriority.Unset); UpdateValueEntry(null, BindingPriority.Animation); - SetAndRaiseCore(owner, (StyledProperty)property, _baseValue!, BasePriority, false); + SetAndRaiseCore(owner, (StyledProperty)property, _baseValue!, BasePriority); } public override void CoerceValue(ValueStore owner, AvaloniaProperty property) @@ -140,24 +143,24 @@ namespace Avalonia.PropertyStore var p = (StyledProperty)property; BindingPriority priority; - T oldValue; + T newValue; if (property.Inherits && owner.TryGetInheritedValue(property, out var i)) { - oldValue = ((EffectiveValue)i).Value; + newValue = ((EffectiveValue)i).Value; priority = BindingPriority.Inherited; } else { - oldValue = _metadata.DefaultValue; + newValue = _metadata.DefaultValue; priority = BindingPriority.Unset; } - if (!EqualityComparer.Default.Equals(oldValue, Value)) + if (!EqualityComparer.Default.Equals(newValue, Value)) { - owner.Owner.RaisePropertyChanged(p, Value, oldValue, priority, true); + owner.Owner.RaisePropertyChanged(p, Value, newValue, priority, true); if (property.Inherits) - owner.OnInheritedEffectiveValueDisposed(p, Value); + owner.OnInheritedEffectiveValueDisposed(p, Value, newValue); } if (ValueEntry?.GetDataValidationState(out _, out _) ?? @@ -168,6 +171,17 @@ namespace Avalonia.PropertyStore } } + protected override void CoerceDefaultValueAndRaise(ValueStore owner, AvaloniaProperty property) + { + Debug.Assert(_uncommon?._coerce is not null); + Debug.Assert(Priority == BindingPriority.Unset); + + var coercedDefaultValue = _uncommon!._coerce!(owner.Owner, _metadata.DefaultValue); + + if (!EqualityComparer.Default.Equals(_metadata.DefaultValue, coercedDefaultValue)) + SetCoercedDefaultValueAndRaise(owner, (StyledProperty)property, coercedDefaultValue); + } + protected override object? GetBoxedValue() => Value; private static T GetValue(IValueEntry entry) @@ -183,7 +197,8 @@ namespace Avalonia.PropertyStore StyledProperty property, T value, BindingPriority priority, - bool isOverriddenCurrentValue) + bool isOverriddenCurrentValue = false, + bool isCoercedDefaultValue = false) { var oldValue = Value; var valueChanged = false; @@ -191,6 +206,7 @@ namespace Avalonia.PropertyStore var v = value; IsOverridenCurrentValue = isOverriddenCurrentValue; + IsCoercedDefaultValue = isCoercedDefaultValue; if (_uncommon?._coerce is { } coerce) v = coerce(owner.Owner, value); diff --git a/src/Avalonia.Base/PropertyStore/ValueStore.cs b/src/Avalonia.Base/PropertyStore/ValueStore.cs index af31459a98..2047f4d2d0 100644 --- a/src/Avalonia.Base/PropertyStore/ValueStore.cs +++ b/src/Avalonia.Base/PropertyStore/ValueStore.cs @@ -259,6 +259,27 @@ namespace Avalonia.PropertyStore { if (_effectiveValues.TryGetValue(property, out var v)) v.CoerceValue(this, property); + else + property.RouteCoerceDefaultValue(Owner); + } + + public void CoerceDefaultValue(StyledProperty property) + { + var metadata = property.GetMetadata(Owner.GetType()); + + if (metadata.CoerceValue is null) + return; + + var coercedDefaultValue = metadata.CoerceValue(Owner, metadata.DefaultValue); + + if (EqualityComparer.Default.Equals(metadata.DefaultValue, coercedDefaultValue)) + return; + + // We have a situation where the default value isn't valid according to the coerce + // function. In this case, we need to create an EffectiveValue entry. + var effectiveValue = CreateEffectiveValue(property); + AddEffectiveValue(property, effectiveValue); + effectiveValue.SetCoercedDefaultValueAndRaise(this, property, coercedDefaultValue); } public Optional GetBaseValue(StyledProperty property) @@ -419,7 +440,9 @@ namespace Avalonia.PropertyStore ReevaluateEffectiveValue(property, current); } else + { ReevaluateEffectiveValues(); + } } /// @@ -481,7 +504,8 @@ namespace Avalonia.PropertyStore /// /// The property whose value changed. /// The old value of the property. - public void OnInheritedEffectiveValueDisposed(StyledProperty property, T oldValue) + /// The new value of the property. + public void OnInheritedEffectiveValueDisposed(StyledProperty property, T oldValue, T newValue) { Debug.Assert(property.Inherits); @@ -489,12 +513,11 @@ namespace Avalonia.PropertyStore if (children is not null) { - var defaultValue = property.GetDefaultValue(Owner.GetType()); var count = children.Count; for (var i = 0; i < count; ++i) { - children[i].GetValueStore().OnAncestorInheritedValueChanged(property, oldValue, defaultValue); + children[i].GetValueStore().OnAncestorInheritedValueChanged(property, oldValue, newValue); } } } @@ -838,20 +861,25 @@ namespace Avalonia.PropertyStore goto restart; } - if (current?.Priority == BindingPriority.Unset) + if (current is not null) { - if (current.BasePriority == BindingPriority.Unset) - { - RemoveEffectiveValue(property); - current.DisposeAndRaiseUnset(this, property); - } - else + current.EndReevaluation(this, property); + + if (current.CanRemove()) { - current.RemoveAnimationAndRaise(this, property); + if (current.BasePriority == BindingPriority.Unset) + { + RemoveEffectiveValue(property); + current.DisposeAndRaiseUnset(this, property); + } + else + { + current.RemoveAnimationAndRaise(this, property); + } } - } - current?.EndReevaluation(); + current.UnsubscribeIfNecessary(); + } } finally { @@ -923,7 +951,9 @@ namespace Avalonia.PropertyStore { _effectiveValues.GetKeyValue(i, out var key, out var e); - if (e.Priority == BindingPriority.Unset && !e.IsOverridenCurrentValue) + e.EndReevaluation(this, key); + + if (e.CanRemove()) { RemoveEffectiveValue(key, i); e.DisposeAndRaiseUnset(this, key); @@ -932,7 +962,7 @@ namespace Avalonia.PropertyStore break; } - e.EndReevaluation(); + e.UnsubscribeIfNecessary(); } } finally diff --git a/src/Avalonia.Base/Reactive/AvaloniaPropertyBindingObservable.cs b/src/Avalonia.Base/Reactive/AvaloniaPropertyBindingObservable.cs index 0789684eff..fd68381d55 100644 --- a/src/Avalonia.Base/Reactive/AvaloniaPropertyBindingObservable.cs +++ b/src/Avalonia.Base/Reactive/AvaloniaPropertyBindingObservable.cs @@ -4,18 +4,21 @@ using Avalonia.Data; namespace Avalonia.Reactive { - internal class AvaloniaPropertyBindingObservable : LightweightObservableBase>, IDescription + internal class AvaloniaPropertyBindingObservable : LightweightObservableBase>, IDescription { private readonly WeakReference _target; private readonly AvaloniaProperty _property; - private BindingValue _value = BindingValue.Unset; + private readonly Func? _converter; + private BindingValue _value = BindingValue.Unset; public AvaloniaPropertyBindingObservable( AvaloniaObject target, - AvaloniaProperty property) + AvaloniaProperty property, + Func? converter = null) { _target = new WeakReference(target); _property = property; + _converter = converter; } public string Description => $"{_target.GetType().Name}.{_property.Name}"; @@ -24,8 +27,17 @@ namespace Avalonia.Reactive { if (_target.TryGetTarget(out var target)) { - _value = (T)target.GetValue(_property)!; - target.PropertyChanged += PropertyChanged; + if (_converter is { } converter) + { + var unconvertedValue = (TSource)target.GetValue(_property)!; + _value = converter(unconvertedValue); + target.PropertyChanged += PropertyChanged_WithConversion; + } + else + { + _value = (TResult)target.GetValue(_property)!; + target.PropertyChanged += PropertyChanged; + } } } @@ -33,11 +45,18 @@ namespace Avalonia.Reactive { if (_target.TryGetTarget(out var target)) { - target.PropertyChanged -= PropertyChanged; + if (_converter is not null) + { + target.PropertyChanged -= PropertyChanged_WithConversion; + } + else + { + target.PropertyChanged -= PropertyChanged; + } } } - protected override void Subscribed(IObserver> observer, bool first) + protected override void Subscribed(IObserver> observer, bool first) { if (_value.Type != BindingValueType.UnsetValue) { @@ -49,27 +68,59 @@ namespace Avalonia.Reactive { if (e.Property == _property) { - if (e is AvaloniaPropertyChangedEventArgs typedArgs) + if (e is AvaloniaPropertyChangedEventArgs typedArgs) { - var newValue = e.Sender.GetValue(typedArgs.Property); + PublishValue(e.Sender.GetValue(typedArgs.Property)); + } + else + { + PublishUntypedValue(e.Sender.GetValue(e.Property)); + } + } + } - if (!_value.HasValue || !EqualityComparer.Default.Equals(newValue, _value.Value)) - { - _value = newValue; - PublishNext(_value); - } + private void PropertyChanged_WithConversion(object? sender, AvaloniaPropertyChangedEventArgs e) + { + if (e.Property == _property) + { + if (e is AvaloniaPropertyChangedEventArgs typedArgs) + { + var newValueRaw = e.Sender.GetValue(typedArgs.Property); + + var newValue = _converter!(newValueRaw); + + PublishValue(newValue); } else { var newValue = e.Sender.GetValue(e.Property); - if (!Equals(newValue, _value)) + if (newValue is TSource source) { - _value = (T)newValue!; - PublishNext(_value); + newValue = _converter!(source); } + + PublishUntypedValue(newValue); } } } + + private void PublishValue(TResult newValue) + { + if (!_value.HasValue || !EqualityComparer.Default.Equals(newValue, _value.Value)) + { + _value = newValue; + PublishNext(_value); + } + } + + private void PublishUntypedValue(object? newValue) + { + if (!Equals(newValue, _value)) + { + _value = (TResult)newValue!; + PublishNext(_value); + } + } } } diff --git a/src/Avalonia.Base/Reactive/AvaloniaPropertyObservable.cs b/src/Avalonia.Base/Reactive/AvaloniaPropertyObservable.cs index a4fa587a50..0d40fa96e6 100644 --- a/src/Avalonia.Base/Reactive/AvaloniaPropertyObservable.cs +++ b/src/Avalonia.Base/Reactive/AvaloniaPropertyObservable.cs @@ -4,18 +4,21 @@ using Avalonia.Data; namespace Avalonia.Reactive { - internal class AvaloniaPropertyObservable : LightweightObservableBase, IDescription + internal class AvaloniaPropertyObservable : LightweightObservableBase, IDescription { private readonly WeakReference _target; private readonly AvaloniaProperty _property; - private Optional _value; + private readonly Func? _converter; + private Optional _value; public AvaloniaPropertyObservable( AvaloniaObject target, - AvaloniaProperty property) + AvaloniaProperty property, + Func? converter = null) { _target = new WeakReference(target); _property = property; + _converter = converter; } public string Description => $"{_target.GetType().Name}.{_property.Name}"; @@ -24,8 +27,17 @@ namespace Avalonia.Reactive { if (_target.TryGetTarget(out var target)) { - _value = (T)target.GetValue(_property)!; - target.PropertyChanged += PropertyChanged; + if (_converter is { } converter) + { + var unconvertedValue = (TSource)target.GetValue(_property)!; + _value = converter(unconvertedValue); + target.PropertyChanged += PropertyChanged_WithConversion; + } + else + { + _value = (TResult)target.GetValue(_property)!; + target.PropertyChanged += PropertyChanged; + } } } @@ -33,13 +45,20 @@ namespace Avalonia.Reactive { if (_target.TryGetTarget(out var target)) { - target.PropertyChanged -= PropertyChanged; + if (_converter is not null) + { + target.PropertyChanged -= PropertyChanged_WithConversion; + } + else + { + target.PropertyChanged -= PropertyChanged; + } } _value = default; } - protected override void Subscribed(IObserver observer, bool first) + protected override void Subscribed(IObserver observer, bool first) { if (_value.HasValue) observer.OnNext(_value.Value); @@ -49,23 +68,49 @@ namespace Avalonia.Reactive { if (e.Property == _property) { - T newValue; + TResult newValue; - if (e is AvaloniaPropertyChangedEventArgs typed) + if (e is AvaloniaPropertyChangedEventArgs typed) { newValue = AvaloniaObjectExtensions.GetValue(e.Sender, typed.Property); } else { - newValue = (T)e.Sender.GetValue(e.Property)!; + newValue = (TResult)e.Sender.GetValue(e.Property)!; } - if (!_value.HasValue || - !EqualityComparer.Default.Equals(newValue, _value.Value)) + PublishNewValue(newValue); + } + } + + private void PropertyChanged_WithConversion(object? sender, AvaloniaPropertyChangedEventArgs e) + { + if (e.Property == _property) + { + TSource newValueRaw; + + if (e is AvaloniaPropertyChangedEventArgs typed) { - _value = newValue; - PublishNext(_value.Value!); + newValueRaw = AvaloniaObjectExtensions.GetValue(e.Sender, typed.Property); } + else + { + newValueRaw = (TSource)e.Sender.GetValue(e.Property)!; + } + + var newValue = _converter!(newValueRaw); + + PublishNewValue(newValue); + } + } + + private void PublishNewValue(TResult newValue) + { + if (!_value.HasValue || + !EqualityComparer.Default.Equals(newValue, _value.Value)) + { + _value = newValue; + PublishNext(_value.Value!); } } } diff --git a/src/Avalonia.Base/Rendering/SceneGraph/CustomDrawOperation.cs b/src/Avalonia.Base/Rendering/SceneGraph/CustomDrawOperation.cs index ff2616bfe4..8f5ccb4e51 100644 --- a/src/Avalonia.Base/Rendering/SceneGraph/CustomDrawOperation.cs +++ b/src/Avalonia.Base/Rendering/SceneGraph/CustomDrawOperation.cs @@ -13,7 +13,7 @@ namespace Avalonia.Rendering.SceneGraph Custom = custom; } - public override bool HitTest(Point p) => Custom.HitTest(p); + public override bool HitTestTransformed(Point p) => Custom.HitTest(p); public override void Render(IDrawingContextImpl context) { diff --git a/src/Avalonia.Base/Rendering/SceneGraph/DrawOperation.cs b/src/Avalonia.Base/Rendering/SceneGraph/DrawOperation.cs index 5b93cd8cfc..786ce28d06 100644 --- a/src/Avalonia.Base/Rendering/SceneGraph/DrawOperation.cs +++ b/src/Avalonia.Base/Rendering/SceneGraph/DrawOperation.cs @@ -37,5 +37,20 @@ namespace Avalonia.Rendering.SceneGraph } public Matrix Transform { get; } + + public sealed override bool HitTest(Point p) + { + if (Transform.IsIdentity) + return HitTestTransformed(p); + + if (!Transform.HasInverse) + return false; + + var transformedPoint = Transform.Invert().Transform(p); + + return HitTestTransformed(transformedPoint); + } + + public abstract bool HitTestTransformed(Point p); } } diff --git a/src/Avalonia.Base/Rendering/SceneGraph/EllipseNode.cs b/src/Avalonia.Base/Rendering/SceneGraph/EllipseNode.cs index d5f0270cb2..0a2b74e46a 100644 --- a/src/Avalonia.Base/Rendering/SceneGraph/EllipseNode.cs +++ b/src/Avalonia.Base/Rendering/SceneGraph/EllipseNode.cs @@ -43,7 +43,7 @@ namespace Avalonia.Rendering.SceneGraph public override void Render(IDrawingContextImpl context) => context.DrawEllipse(Brush, Pen, Rect); - public override bool HitTest(Point p) + public override bool HitTestTransformed(Point p) { var center = Rect.Center; diff --git a/src/Avalonia.Base/Rendering/SceneGraph/ExperimentalAcrylicNode.cs b/src/Avalonia.Base/Rendering/SceneGraph/ExperimentalAcrylicNode.cs index e1f79e0e10..22fc49d30e 100644 --- a/src/Avalonia.Base/Rendering/SceneGraph/ExperimentalAcrylicNode.cs +++ b/src/Avalonia.Base/Rendering/SceneGraph/ExperimentalAcrylicNode.cs @@ -65,6 +65,6 @@ namespace Avalonia.Rendering.SceneGraph } /// - public override bool HitTest(Point p) => Rect.Rect.ContainsExclusive(p); + public override bool HitTestTransformed(Point p) => Rect.Rect.ContainsExclusive(p); } } diff --git a/src/Avalonia.Base/Rendering/SceneGraph/GeometryNode.cs b/src/Avalonia.Base/Rendering/SceneGraph/GeometryNode.cs index f64a3e845d..48af3b0e6b 100644 --- a/src/Avalonia.Base/Rendering/SceneGraph/GeometryNode.cs +++ b/src/Avalonia.Base/Rendering/SceneGraph/GeometryNode.cs @@ -64,7 +64,7 @@ namespace Avalonia.Rendering.SceneGraph } /// - public override bool HitTest(Point p) + public override bool HitTestTransformed(Point p) { return (Brush != null && Geometry.FillContains(p)) || (Pen != null && Geometry.StrokeContains(Pen, p)); diff --git a/src/Avalonia.Base/Rendering/SceneGraph/GlyphRunNode.cs b/src/Avalonia.Base/Rendering/SceneGraph/GlyphRunNode.cs index 5b975e29e1..b4e28dc254 100644 --- a/src/Avalonia.Base/Rendering/SceneGraph/GlyphRunNode.cs +++ b/src/Avalonia.Base/Rendering/SceneGraph/GlyphRunNode.cs @@ -53,7 +53,7 @@ namespace Avalonia.Rendering.SceneGraph } /// - public override bool HitTest(Point p) => Bounds.ContainsExclusive(p); + public override bool HitTestTransformed(Point p) => Bounds.ContainsExclusive(p); public override void Dispose() { diff --git a/src/Avalonia.Base/Rendering/SceneGraph/ImageNode.cs b/src/Avalonia.Base/Rendering/SceneGraph/ImageNode.cs index dd9787e8d1..ac946cc8b2 100644 --- a/src/Avalonia.Base/Rendering/SceneGraph/ImageNode.cs +++ b/src/Avalonia.Base/Rendering/SceneGraph/ImageNode.cs @@ -94,7 +94,7 @@ namespace Avalonia.Rendering.SceneGraph } /// - public override bool HitTest(Point p) => DestRect.ContainsExclusive(p); + public override bool HitTestTransformed(Point p) => DestRect.ContainsExclusive(p); public override void Dispose() { diff --git a/src/Avalonia.Base/Rendering/SceneGraph/LineNode.cs b/src/Avalonia.Base/Rendering/SceneGraph/LineNode.cs index 61bffc3260..1ac6cffe0a 100644 --- a/src/Avalonia.Base/Rendering/SceneGraph/LineNode.cs +++ b/src/Avalonia.Base/Rendering/SceneGraph/LineNode.cs @@ -66,7 +66,7 @@ namespace Avalonia.Rendering.SceneGraph context.DrawLine(Pen, P1, P2); } - public override bool HitTest(Point p) + public override bool HitTestTransformed(Point p) { var halfThickness = Pen.Thickness / 2; var minX = Math.Min(P1.X, P2.X) - halfThickness; diff --git a/src/Avalonia.Base/Rendering/SceneGraph/OpacityMaskNode.cs b/src/Avalonia.Base/Rendering/SceneGraph/OpacityMaskNode.cs index b0584038a8..1c79a67944 100644 --- a/src/Avalonia.Base/Rendering/SceneGraph/OpacityMaskNode.cs +++ b/src/Avalonia.Base/Rendering/SceneGraph/OpacityMaskNode.cs @@ -30,7 +30,7 @@ namespace Avalonia.Rendering.SceneGraph /// - public override bool HitTest(Point p) => false; + public override bool HitTestTransformed(Point p) => false; /// /// Determines if this draw operation equals another. diff --git a/src/Avalonia.Base/Rendering/SceneGraph/RectangleNode.cs b/src/Avalonia.Base/Rendering/SceneGraph/RectangleNode.cs index 94f61df47d..e85992be34 100644 --- a/src/Avalonia.Base/Rendering/SceneGraph/RectangleNode.cs +++ b/src/Avalonia.Base/Rendering/SceneGraph/RectangleNode.cs @@ -74,7 +74,7 @@ namespace Avalonia.Rendering.SceneGraph public override void Render(IDrawingContextImpl context) => context.DrawRectangle(Brush, Pen, Rect, BoxShadows); /// - public override bool HitTest(Point p) + public override bool HitTestTransformed(Point p) { if (Brush != null) { diff --git a/src/Avalonia.Base/StyledElement.cs b/src/Avalonia.Base/StyledElement.cs index b51093b40c..731cb97161 100644 --- a/src/Avalonia.Base/StyledElement.cs +++ b/src/Avalonia.Base/StyledElement.cs @@ -891,7 +891,7 @@ namespace Avalonia for (var i = 0; i < logicalChildrenCount; i++) { - if (logicalChildren[i] is StyledElement child) + if (logicalChildren[i] is StyledElement child && child._logicalRoot != e.Root) // child may already have been attached within an event handler { child.OnAttachedToLogicalTreeCore(e); } diff --git a/src/Avalonia.Base/StyledProperty.cs b/src/Avalonia.Base/StyledProperty.cs index e1b88cde49..5cb330eda9 100644 --- a/src/Avalonia.Base/StyledProperty.cs +++ b/src/Avalonia.Base/StyledProperty.cs @@ -176,6 +176,11 @@ namespace Avalonia o.ClearValue(this); } + internal override void RouteCoerceDefaultValue(AvaloniaObject o) + { + o.GetValueStore().CoerceDefaultValue(this); + } + /// internal override object? RouteGetValue(AvaloniaObject o) { diff --git a/src/Avalonia.Base/Threading/Dispatcher.Invoke.cs b/src/Avalonia.Base/Threading/Dispatcher.Invoke.cs index c6e9203f70..6842e4a255 100644 --- a/src/Avalonia.Base/Threading/Dispatcher.Invoke.cs +++ b/src/Avalonia.Base/Threading/Dispatcher.Invoke.cs @@ -557,10 +557,10 @@ public partial class Dispatcher /// /// An task that completes after the task returned from callback finishes /// - public Task InvokeTaskAsync(Func callback, DispatcherPriority priority = default) + public Task InvokeAsync(Func callback, DispatcherPriority priority = default) { _ = callback ?? throw new ArgumentNullException(nameof(callback)); - return InvokeAsync(callback, priority).GetTask().Unwrap(); + return InvokeAsync(callback, priority).GetTask().Unwrap(); } /// @@ -578,10 +578,10 @@ public partial class Dispatcher /// /// An task that completes after the task returned from callback finishes /// - public Task InvokeTaskAsync(Func> action, DispatcherPriority priority = default) + public Task InvokeAsync(Func> action, DispatcherPriority priority = default) { _ = action ?? throw new ArgumentNullException(nameof(action)); - return InvokeAsync(action, priority).GetTask().Unwrap(); + return InvokeAsync>(action, priority).GetTask().Unwrap(); } /// diff --git a/src/Avalonia.Base/Visual.cs b/src/Avalonia.Base/Visual.cs index b4c5b2a1d2..05159eb4ae 100644 --- a/src/Avalonia.Base/Visual.cs +++ b/src/Avalonia.Base/Visual.cs @@ -487,7 +487,7 @@ namespace Avalonia for (var i = 0; i < visualChildrenCount; i++) { - if (visualChildren[i] is { } child) + if (visualChildren[i] is { } child && child._visualRoot != e.Root) // child may already have been attached within an event handler { child.OnAttachedToVisualTreeCore(e); } diff --git a/src/Avalonia.Controls.ColorPicker/ColorView/AlphaComponentPosition.cs b/src/Avalonia.Controls.ColorPicker/AlphaComponentPosition.cs similarity index 100% rename from src/Avalonia.Controls.ColorPicker/ColorView/AlphaComponentPosition.cs rename to src/Avalonia.Controls.ColorPicker/AlphaComponentPosition.cs diff --git a/src/Avalonia.Controls.ColorPicker/ColorView/ColorView.Properties.cs b/src/Avalonia.Controls.ColorPicker/ColorView/ColorView.Properties.cs index e334a1d323..532e87a9fc 100644 --- a/src/Avalonia.Controls.ColorPicker/ColorView/ColorView.Properties.cs +++ b/src/Avalonia.Controls.ColorPicker/ColorView/ColorView.Properties.cs @@ -48,7 +48,7 @@ namespace Avalonia.Controls public static readonly StyledProperty HexInputAlphaPositionProperty = AvaloniaProperty.Register( nameof(HexInputAlphaPosition), - AlphaComponentPosition.Trailing); // Match CSS (and default slider order) instead of XAML/WinUI + AlphaComponentPosition.Leading); // By default match XAML and the WinUI control /// /// Defines the property. diff --git a/src/Avalonia.Controls.ColorPicker/ColorView/ColorView.cs b/src/Avalonia.Controls.ColorPicker/ColorView/ColorView.cs index 274e7f5851..7674b74b6a 100644 --- a/src/Avalonia.Controls.ColorPicker/ColorView/ColorView.cs +++ b/src/Avalonia.Controls.ColorPicker/ColorView/ColorView.cs @@ -61,7 +61,11 @@ namespace Avalonia.Controls { if (_hexTextBox != null) { - _hexTextBox.Text = ColorToHexConverter.ToHexString(Color, HexInputAlphaPosition); + _hexTextBox.Text = ColorToHexConverter.ToHexString( + Color, + HexInputAlphaPosition, + includeAlpha: (IsAlphaEnabled && IsAlphaVisible), + includeSymbol: false); } } diff --git a/src/Avalonia.Controls.ColorPicker/Converters/ColorToHexConverter.cs b/src/Avalonia.Controls.ColorPicker/Converters/ColorToHexConverter.cs index 8798f874f4..8257499d70 100644 --- a/src/Avalonia.Controls.ColorPicker/Converters/ColorToHexConverter.cs +++ b/src/Avalonia.Controls.ColorPicker/Converters/ColorToHexConverter.cs @@ -11,6 +11,18 @@ namespace Avalonia.Controls.Converters /// public class ColorToHexConverter : IValueConverter { + /// + /// Gets or sets a value indicating whether the alpha component is visible in the Hex formatted text. + /// + /// + /// When hidden the existing alpha component value is maintained. Also when hidden the user is still + /// able to input an 8-digit number with alpha. Alpha will be processed but then removed when displayed. + /// + /// Because this property only controls whether alpha is displayed (and it is still processed regardless) + /// it is termed 'Visible' instead of 'Enabled'. + /// + public bool IsAlphaVisible { get; set; } = true; + /// /// Gets or sets the position of a color's alpha component relative to all other components. /// @@ -48,7 +60,7 @@ namespace Avalonia.Controls.Converters return AvaloniaProperty.UnsetValue; } - return ToHexString(color, AlphaPosition, includeSymbol); + return ToHexString(color, AlphaPosition, IsAlphaVisible, includeSymbol); } /// @@ -67,26 +79,40 @@ namespace Avalonia.Controls.Converters /// /// The color to represent as a hex value string. /// The output position of the alpha component. + /// Whether the alpha component will be included in the hex string. /// Whether the hex symbol '#' will be added. /// The input color converted to its hex value string. public static string ToHexString( Color color, AlphaComponentPosition alphaPosition, + bool includeAlpha = true, bool includeSymbol = false) { uint intColor; - if (alphaPosition == AlphaComponentPosition.Trailing) + string hexColor; + + if (includeAlpha) { - intColor = ((uint)color.R << 24) | ((uint)color.G << 16) | ((uint)color.B << 8) | (uint)color.A; + if (alphaPosition == AlphaComponentPosition.Trailing) + { + intColor = ((uint)color.R << 24) | ((uint)color.G << 16) | ((uint)color.B << 8) | (uint)color.A; + } + else + { + // Default is Leading alpha (same as XAML) + intColor = ((uint)color.A << 24) | ((uint)color.R << 16) | ((uint)color.G << 8) | (uint)color.B; + } + + hexColor = intColor.ToString("x8", CultureInfo.InvariantCulture).ToUpperInvariant(); } else { - // Default is Leading alpha - intColor = ((uint)color.A << 24) | ((uint)color.R << 16) | ((uint)color.G << 8) | (uint)color.B; + // In this case the alpha position no longer matters + // Both cases are calculated the same + intColor = ((uint)color.R << 16) | ((uint)color.G << 8) | (uint)color.B; + hexColor = intColor.ToString("x6", CultureInfo.InvariantCulture).ToUpperInvariant(); } - string hexColor = intColor.ToString("x8", CultureInfo.InvariantCulture).ToUpperInvariant(); - if (includeSymbol) { hexColor = '#' + hexColor; diff --git a/src/Avalonia.Controls.ColorPicker/Themes/Fluent/ColorPicker.xaml b/src/Avalonia.Controls.ColorPicker/Themes/Fluent/ColorPicker.xaml index a9f52b93c7..b3c7cd9f9c 100644 --- a/src/Avalonia.Controls.ColorPicker/Themes/Fluent/ColorPicker.xaml +++ b/src/Avalonia.Controls.ColorPicker/Themes/Fluent/ColorPicker.xaml @@ -6,6 +6,8 @@ + + diff --git a/src/Avalonia.Controls.ColorPicker/Themes/Fluent/ColorPreviewer.xaml b/src/Avalonia.Controls.ColorPicker/Themes/Fluent/ColorPreviewer.xaml index 3a88d25ef1..e05fa5a907 100644 --- a/src/Avalonia.Controls.ColorPicker/Themes/Fluent/ColorPreviewer.xaml +++ b/src/Avalonia.Controls.ColorPicker/Themes/Fluent/ColorPreviewer.xaml @@ -64,6 +64,7 @@ diff --git a/src/Avalonia.Controls.ColorPicker/Themes/Fluent/ColorView.xaml b/src/Avalonia.Controls.ColorPicker/Themes/Fluent/ColorView.xaml index f72fb11bbe..acd2c7ff15 100644 --- a/src/Avalonia.Controls.ColorPicker/Themes/Fluent/ColorView.xaml +++ b/src/Avalonia.Controls.ColorPicker/Themes/Fluent/ColorView.xaml @@ -295,6 +295,8 @@ + + diff --git a/src/Avalonia.Controls.ColorPicker/Themes/Simple/ColorPicker.xaml b/src/Avalonia.Controls.ColorPicker/Themes/Simple/ColorPicker.xaml index 7639073775..ff4e1d93a8 100644 --- a/src/Avalonia.Controls.ColorPicker/Themes/Simple/ColorPicker.xaml +++ b/src/Avalonia.Controls.ColorPicker/Themes/Simple/ColorPicker.xaml @@ -6,6 +6,8 @@ + + diff --git a/src/Avalonia.Controls.ColorPicker/Themes/Simple/ColorPreviewer.xaml b/src/Avalonia.Controls.ColorPicker/Themes/Simple/ColorPreviewer.xaml index 0e51a0519a..a39dd91f52 100644 --- a/src/Avalonia.Controls.ColorPicker/Themes/Simple/ColorPreviewer.xaml +++ b/src/Avalonia.Controls.ColorPicker/Themes/Simple/ColorPreviewer.xaml @@ -64,6 +64,7 @@ diff --git a/src/Avalonia.Controls.ColorPicker/Themes/Simple/ColorView.xaml b/src/Avalonia.Controls.ColorPicker/Themes/Simple/ColorView.xaml index 4e219a98af..a26d3179b5 100644 --- a/src/Avalonia.Controls.ColorPicker/Themes/Simple/ColorView.xaml +++ b/src/Avalonia.Controls.ColorPicker/Themes/Simple/ColorView.xaml @@ -257,6 +257,8 @@ + + diff --git a/src/Avalonia.Controls.DataGrid/DataGridColumnHeader.cs b/src/Avalonia.Controls.DataGrid/DataGridColumnHeader.cs index b3e106a7bf..5250f80f77 100644 --- a/src/Avalonia.Controls.DataGrid/DataGridColumnHeader.cs +++ b/src/Avalonia.Controls.DataGrid/DataGridColumnHeader.cs @@ -440,13 +440,10 @@ namespace Avalonia.Controls } Debug.Assert(OwningGrid.Parent is InputElement); - - double distanceFromLeft = mousePosition.X; - double distanceFromRight = Bounds.Width - distanceFromLeft; - + OnMouseMove_Resize(ref handled, mousePositionHeaders); - OnMouseMove_Reorder(ref handled, mousePosition, mousePositionHeaders, distanceFromLeft, distanceFromRight); + OnMouseMove_Reorder(ref handled, mousePosition, mousePositionHeaders); SetDragCursor(mousePosition); } @@ -716,7 +713,7 @@ namespace Avalonia.Controls } //TODO DragEvents - private void OnMouseMove_Reorder(ref bool handled, Point mousePosition, Point mousePositionHeaders, double distanceFromLeft, double distanceFromRight) + private void OnMouseMove_Reorder(ref bool handled, Point mousePosition, Point mousePositionHeaders) { if (handled) { @@ -724,7 +721,7 @@ namespace Avalonia.Controls } //handle entry into reorder mode - if (_dragMode == DragMode.MouseDown && _dragColumn == null && _lastMousePositionHeaders != null && (distanceFromRight > DATAGRIDCOLUMNHEADER_resizeRegionWidth && distanceFromLeft > DATAGRIDCOLUMNHEADER_resizeRegionWidth)) + if (_dragMode == DragMode.MouseDown && _dragColumn == null && _lastMousePositionHeaders != null) { var distanceFromInitial = (Vector)(mousePositionHeaders - _lastMousePositionHeaders); if (distanceFromInitial.Length > DATAGRIDCOLUMNHEADER_columnsDragTreshold) diff --git a/src/Avalonia.Controls/Automation/Peers/ControlAutomationPeer.cs b/src/Avalonia.Controls/Automation/Peers/ControlAutomationPeer.cs index e8fb6b75ad..d04dfec3e8 100644 --- a/src/Avalonia.Controls/Automation/Peers/ControlAutomationPeer.cs +++ b/src/Avalonia.Controls/Automation/Peers/ControlAutomationPeer.cs @@ -88,10 +88,10 @@ namespace Avalonia.Automation.Peers if (string.IsNullOrWhiteSpace(result) && GetLabeledBy() is AutomationPeer labeledBy) { - return labeledBy.GetName(); + result = labeledBy.GetName(); } - return null; + return result; } protected override AutomationPeer? GetParentCore() diff --git a/src/Avalonia.Controls/Chrome/CaptionButtons.cs b/src/Avalonia.Controls/Chrome/CaptionButtons.cs index f5fcbed9fb..90dd14c703 100644 --- a/src/Avalonia.Controls/Chrome/CaptionButtons.cs +++ b/src/Avalonia.Controls/Chrome/CaptionButtons.cs @@ -15,6 +15,7 @@ namespace Avalonia.Controls.Chrome [PseudoClasses(":minimized", ":normal", ":maximized", ":fullscreen")] public class CaptionButtons : TemplatedControl { + private Button? _restoreButton; private IDisposable? _disposables; /// @@ -28,14 +29,23 @@ namespace Avalonia.Controls.Chrome { HostWindow = hostWindow; - _disposables = HostWindow.GetObservable(Window.WindowStateProperty) - .Subscribe(x => - { - PseudoClasses.Set(":minimized", x == WindowState.Minimized); - PseudoClasses.Set(":normal", x == WindowState.Normal); - PseudoClasses.Set(":maximized", x == WindowState.Maximized); - PseudoClasses.Set(":fullscreen", x == WindowState.FullScreen); - }); + _disposables = new CompositeDisposable + { + HostWindow.GetObservable(Window.CanResizeProperty) + .Subscribe(x => + { + if (_restoreButton is not null) + _restoreButton.IsEnabled = x; + }), + HostWindow.GetObservable(Window.WindowStateProperty) + .Subscribe(x => + { + PseudoClasses.Set(":minimized", x == WindowState.Minimized); + PseudoClasses.Set(":normal", x == WindowState.Normal); + PseudoClasses.Set(":maximized", x == WindowState.Maximized); + PseudoClasses.Set(":fullscreen", x == WindowState.FullScreen); + }), + }; } } @@ -94,6 +104,9 @@ namespace Avalonia.Controls.Chrome restoreButton.Click += (sender, e) => OnRestore(); minimiseButton.Click += (sender, e) => OnMinimize(); fullScreenButton.Click += (sender, e) => OnToggleFullScreen(); + + restoreButton.IsEnabled = HostWindow?.CanResize ?? true; + _restoreButton = restoreButton; } } } diff --git a/src/Avalonia.Controls/Flyouts/Flyout.cs b/src/Avalonia.Controls/Flyouts/Flyout.cs index c0d3600c1e..cebcbb6562 100644 --- a/src/Avalonia.Controls/Flyouts/Flyout.cs +++ b/src/Avalonia.Controls/Flyouts/Flyout.cs @@ -13,12 +13,22 @@ namespace Avalonia.Controls public static readonly StyledProperty ContentProperty = AvaloniaProperty.Register(nameof(Content)); + private Classes? _classes; + /// /// Gets the Classes collection to apply to the FlyoutPresenter this Flyout is hosting /// - public Classes FlyoutPresenterClasses => _classes ??= new Classes(); - - private Classes? _classes; + public Classes FlyoutPresenterClasses + { + get => _classes ??= new Classes(); + set + { + if (_classes is null) + _classes = value; + else if (_classes != value) + _classes.Replace(value); + } + } /// /// Defines the property. diff --git a/src/Avalonia.Controls/ISelectable.cs b/src/Avalonia.Controls/ISelectable.cs index 144adaa2f5..d0da7c4932 100644 --- a/src/Avalonia.Controls/ISelectable.cs +++ b/src/Avalonia.Controls/ISelectable.cs @@ -1,16 +1,9 @@ -using Avalonia.Controls.Primitives; - namespace Avalonia.Controls { /// - /// Interface for objects that are selectable. + /// An interface that is implemented by objects that expose their selection state via a + /// boolean property. /// - /// - /// Controls such as use this interface to indicate the - /// selected control in a list. If changing the control's property - /// should update the selection in a or equivalent, then - /// the control should raise the . - /// public interface ISelectable { /// @@ -18,4 +11,4 @@ namespace Avalonia.Controls /// bool IsSelected { get; set; } } -} \ No newline at end of file +} diff --git a/src/Avalonia.Controls/ItemsControl.cs b/src/Avalonia.Controls/ItemsControl.cs index 6a91428147..60b0f8b193 100644 --- a/src/Avalonia.Controls/ItemsControl.cs +++ b/src/Avalonia.Controls/ItemsControl.cs @@ -94,7 +94,6 @@ namespace Avalonia.Controls private ItemContainerGenerator? _itemContainerGenerator; private EventHandler? _childIndexChanged; private IDataTemplate? _displayMemberItemTemplate; - private ScrollViewer? _scrollViewer; private ItemsPresenter? _itemsPresenter; /// @@ -409,14 +408,35 @@ namespace Avalonia.Controls ic.ItemContainerTheme = ict; } - // This condition is separate because HeaderedItemsControl needs to also run the - // ItemsControl preparation. + // These conditions are separate because HeaderedItemsControl and + // HeaderedSelectingItemsControl also need to run the ItemsControl preparation. if (container is HeaderedItemsControl hic) { hic.Header = item; hic.HeaderTemplate = itemTemplate; - hic.PrepareItemContainer(); + hic.PrepareItemContainer(this); } + else if (container is HeaderedSelectingItemsControl hsic) + { + hsic.Header = item; + hsic.HeaderTemplate = itemTemplate; + hsic.PrepareItemContainer(this); + } + } + + /// + /// Called when a container has been fully prepared to display an item. + /// + /// The container control. + /// The item being displayed. + /// The index of the item being displayed. + /// + /// This method will be called when a container has been fully prepared and added to the + /// logical and visual trees, but may be called before a layout pass has completed. It is + /// called immediately before the event is raised. + /// + protected internal virtual void ContainerForItemPreparedOverride(Control container, object? item, int index) + { } /// @@ -436,6 +456,34 @@ namespace Avalonia.Controls /// The container element. protected internal virtual void ClearContainerForItemOverride(Control container) { + if (container is HeaderedContentControl hcc) + { + if (hcc.Content is Control) + hcc.Content = null; + if (hcc.Header is Control) + hcc.Header = null; + } + else if (container is ContentControl cc) + { + if (cc.Content is Control) + cc.Content = null; + } + else if (container is ContentPresenter p) + { + if (p.Content is Control) + p.Content = null; + } + else if (container is HeaderedItemsControl hic) + { + if (hic.Header is Control) + hic.Header = null; + } + else if (container is HeaderedSelectingItemsControl hsic) + { + if (hsic.Header is Control) + hsic.Header = null; + } + // Feels like we should be clearing the HeaderedItemsControl.Items binding here, but looking at // the WPF source it seems that this isn't done there. } @@ -451,7 +499,6 @@ namespace Avalonia.Controls protected override void OnApplyTemplate(TemplateAppliedEventArgs e) { base.OnApplyTemplate(e); - _scrollViewer = e.NameScope.Find("PART_ScrollViewer"); _itemsPresenter = e.NameScope.Find("PART_ItemsPresenter"); } @@ -622,8 +669,8 @@ namespace Avalonia.Controls internal void ItemContainerPrepared(Control container, object? item, int index) { + ContainerForItemPreparedOverride(container, item, index); _childIndexChanged?.Invoke(this, new ChildIndexChangedEventArgs(container, index)); - _scrollViewer?.RegisterAnchorCandidate(container); ContainerPrepared?.Invoke(this, new(container, index)); } @@ -636,7 +683,6 @@ namespace Avalonia.Controls internal void ClearItemContainer(Control container) { - _scrollViewer?.UnregisterAnchorCandidate(container); ClearContainerForItemOverride(container); ContainerClearing?.Invoke(this, new(container)); } diff --git a/src/Avalonia.Controls/ListBoxItem.cs b/src/Avalonia.Controls/ListBoxItem.cs index 66a46cab4a..0a6873cd59 100644 --- a/src/Avalonia.Controls/ListBoxItem.cs +++ b/src/Avalonia.Controls/ListBoxItem.cs @@ -1,6 +1,7 @@ using Avalonia.Automation.Peers; using Avalonia.Controls.Metadata; using Avalonia.Controls.Mixins; +using Avalonia.Controls.Primitives; namespace Avalonia.Controls { @@ -14,7 +15,7 @@ namespace Avalonia.Controls /// Defines the property. /// public static readonly StyledProperty IsSelectedProperty = - AvaloniaProperty.Register(nameof(IsSelected)); + SelectingItemsControl.IsSelectedProperty.AddOwner(); /// /// Initializes static members of the class. diff --git a/src/Avalonia.Controls/MenuItem.cs b/src/Avalonia.Controls/MenuItem.cs index 06d84e715d..a0dbf33a1d 100644 --- a/src/Avalonia.Controls/MenuItem.cs +++ b/src/Avalonia.Controls/MenuItem.cs @@ -57,7 +57,7 @@ namespace Avalonia.Controls /// Defines the property. /// public static readonly StyledProperty IsSelectedProperty = - ListBoxItem.IsSelectedProperty.AddOwner(); + SelectingItemsControl.IsSelectedProperty.AddOwner(); /// /// Defines the property. diff --git a/src/Avalonia.Controls/Platform/IInsetsManager.cs b/src/Avalonia.Controls/Platform/IInsetsManager.cs index 072bace154..c604b89e5c 100644 --- a/src/Avalonia.Controls/Platform/IInsetsManager.cs +++ b/src/Avalonia.Controls/Platform/IInsetsManager.cs @@ -1,4 +1,5 @@ using System; +using Avalonia.Media; using Avalonia.Metadata; #nullable enable @@ -22,7 +23,12 @@ namespace Avalonia.Controls.Platform /// Gets the current safe area padding. /// Thickness SafeAreaPadding { get; } - + + /// + /// Gets or sets the color of the platform's system bars + /// + Color? SystemBarColor { get; set; } + /// /// Occurs when safe area for the current window changes. /// diff --git a/src/Avalonia.Controls/Presenters/ScrollContentPresenter.cs b/src/Avalonia.Controls/Presenters/ScrollContentPresenter.cs index bc86558ab3..1704965fd7 100644 --- a/src/Avalonia.Controls/Presenters/ScrollContentPresenter.cs +++ b/src/Avalonia.Controls/Presenters/ScrollContentPresenter.cs @@ -6,6 +6,7 @@ using Avalonia.Input; using Avalonia.Input.GestureRecognizers; using Avalonia.Utilities; using Avalonia.VisualTree; +using System.Linq; namespace Avalonia.Controls.Presenters { @@ -19,44 +20,34 @@ namespace Avalonia.Controls.Presenters /// /// Defines the property. /// - public static readonly DirectProperty CanHorizontallyScrollProperty = - AvaloniaProperty.RegisterDirect( - nameof(CanHorizontallyScroll), - o => o.CanHorizontallyScroll, - (o, v) => o.CanHorizontallyScroll = v); + public static readonly StyledProperty CanHorizontallyScrollProperty = + AvaloniaProperty.Register(nameof(CanHorizontallyScroll)); /// /// Defines the property. /// - public static readonly DirectProperty CanVerticallyScrollProperty = - AvaloniaProperty.RegisterDirect( - nameof(CanVerticallyScroll), - o => o.CanVerticallyScroll, - (o, v) => o.CanVerticallyScroll = v); + public static readonly StyledProperty CanVerticallyScrollProperty = + AvaloniaProperty.Register(nameof(CanVerticallyScroll)); /// /// Defines the property. /// public static readonly DirectProperty ExtentProperty = ScrollViewer.ExtentProperty.AddOwner( - o => o.Extent, - (o, v) => o.Extent = v); + o => o.Extent); /// /// Defines the property. /// - public static readonly DirectProperty OffsetProperty = - ScrollViewer.OffsetProperty.AddOwner( - o => o.Offset, - (o, v) => o.Offset = v); + public static readonly StyledProperty OffsetProperty = + ScrollViewer.OffsetProperty.AddOwner(new(coerce: ScrollViewer.CoerceOffset)); /// /// Defines the property. /// public static readonly DirectProperty ViewportProperty = ScrollViewer.ViewportProperty.AddOwner( - o => o.Viewport, - (o, v) => o.Viewport = v); + o => o.Viewport); /// /// Defines the property. @@ -88,16 +79,13 @@ namespace Avalonia.Controls.Presenters public static readonly StyledProperty IsScrollChainingEnabledProperty = ScrollViewer.IsScrollChainingEnabledProperty.AddOwner(); - private bool _canHorizontallyScroll; - private bool _canVerticallyScroll; private bool _arranging; private Size _extent; - private Vector _offset; private IDisposable? _logicalScrollSubscription; private Size _viewport; private Dictionary? _activeLogicalGestureScrolls; private Dictionary? _scrollGestureSnapPoints; - private List? _anchorCandidates; + private HashSet? _anchorCandidates; private Control? _anchorElement; private Rect _anchorElementBounds; private bool _isAnchorElementDirty; @@ -109,6 +97,8 @@ namespace Avalonia.Controls.Presenters private double _verticalSnapPoint; private double _verticalSnapPointOffset; private double _horizontalSnapPointOffset; + private CompositeDisposable? _ownerSubscriptions; + private ScrollViewer? _owner; /// /// Initializes static members of the class. @@ -116,7 +106,6 @@ namespace Avalonia.Controls.Presenters static ScrollContentPresenter() { ClipToBoundsProperty.OverrideDefaultValue(typeof(ScrollContentPresenter), true); - ChildProperty.Changed.AddClassHandler((x, e) => x.ChildChanged(e)); } /// @@ -137,8 +126,8 @@ namespace Avalonia.Controls.Presenters /// public bool CanHorizontallyScroll { - get { return _canHorizontallyScroll; } - set { SetAndRaise(CanHorizontallyScrollProperty, ref _canHorizontallyScroll, value); } + get => GetValue(CanHorizontallyScrollProperty); + set => SetValue(CanHorizontallyScrollProperty, value); } /// @@ -146,8 +135,8 @@ namespace Avalonia.Controls.Presenters /// public bool CanVerticallyScroll { - get { return _canVerticallyScroll; } - set { SetAndRaise(CanVerticallyScrollProperty, ref _canVerticallyScroll, value); } + get => GetValue(CanVerticallyScrollProperty); + set => SetValue(CanVerticallyScrollProperty, value); } /// @@ -164,8 +153,8 @@ namespace Avalonia.Controls.Presenters /// public Vector Offset { - get { return _offset; } - set { SetAndRaise(OffsetProperty, ref _offset, ScrollViewer.CoerceOffset(Extent, Viewport, value)); } + get => GetValue(OffsetProperty); + set => SetValue(OffsetProperty, value); } /// @@ -295,12 +284,60 @@ namespace Avalonia.Controls.Presenters if (result) { - Offset = offset; + SetCurrentValue(OffsetProperty, offset); } return result; } + protected override void OnAttachedToVisualTree(VisualTreeAttachmentEventArgs e) + { + base.OnAttachedToVisualTree(e); + AttachToScrollViewer(); + } + + /// + /// Locates the first ancestor and binds to it. Properties which have been set through other means are not bound. + /// + /// + /// This method is automatically called when the control is attached to a visual tree. + /// + protected internal virtual void AttachToScrollViewer() + { + var owner = this.FindAncestorOfType(); + + if (owner == null) + { + _owner = null; + _ownerSubscriptions?.Dispose(); + _ownerSubscriptions = null; + return; + } + + if (owner == _owner) + { + return; + } + + _ownerSubscriptions?.Dispose(); + + var subscriptionDisposables = new IDisposable?[] + { + IfUnset(CanHorizontallyScrollProperty, p => Bind(p, owner.GetObservable(ScrollViewer.HorizontalScrollBarVisibilityProperty, NotDisabled), Data.BindingPriority.Template)), + IfUnset(CanVerticallyScrollProperty, p => Bind(p, owner.GetObservable(ScrollViewer.VerticalScrollBarVisibilityProperty, NotDisabled), Data.BindingPriority.Template)), + IfUnset(OffsetProperty, p => Bind(p, owner.GetBindingObservable(ScrollViewer.OffsetProperty), Data.BindingPriority.Template)), + IfUnset(IsScrollChainingEnabledProperty, p => Bind(p, owner.GetBindingObservable(ScrollViewer.IsScrollChainingEnabledProperty), Data.BindingPriority.Template)), + IfUnset(ContentProperty, p => Bind(p, owner.GetBindingObservable(ContentProperty), Data.BindingPriority.Template)), + }.Where(d => d != null).Cast().ToArray(); + + _owner = owner; + _ownerSubscriptions = new CompositeDisposable(subscriptionDisposables); + + static bool NotDisabled(ScrollBarVisibility v) => v != ScrollBarVisibility.Disabled; + + IDisposable? IfUnset(T property, Func func) where T : AvaloniaProperty => IsSet(property) ? null : func(property); + } + /// void IScrollAnchorProvider.RegisterAnchorCandidate(Control element) { @@ -310,7 +347,7 @@ namespace Avalonia.Controls.Presenters "An anchor control must be a visual descendent of the ScrollContentPresenter."); } - _anchorCandidates ??= new List(); + _anchorCandidates ??= new(); _anchorCandidates.Add(element); _isAnchorElementDirty = true; } @@ -410,7 +447,7 @@ namespace Avalonia.Controls.Presenters try { _arranging = true; - Offset = newOffset; + SetCurrentValue(OffsetProperty, newOffset); } finally { @@ -427,7 +464,6 @@ namespace Avalonia.Controls.Presenters Viewport = finalSize; Extent = Child!.Bounds.Size.Inflate(Child.Margin); - Offset = ScrollViewer.CoerceOffset(Extent, finalSize, Offset); _isAnchorElementDirty = true; return finalSize; @@ -516,7 +552,7 @@ namespace Avalonia.Controls.Presenters } bool offsetChanged = newOffset != Offset; - Offset = newOffset; + SetCurrentValue(OffsetProperty, newOffset); e.Handled = !IsScrollChainingEnabled || offsetChanged; @@ -529,7 +565,7 @@ namespace Avalonia.Controls.Presenters _activeLogicalGestureScrolls?.Remove(e.Id); _scrollGestureSnapPoints?.Remove(e.Id); - Offset = SnapOffset(Offset); + SetCurrentValue(OffsetProperty, SnapOffset(Offset)); } private void OnScrollGestureInertiaStartingEnded(object? sender, ScrollGestureInertiaStartingEventArgs e) @@ -623,7 +659,7 @@ namespace Avalonia.Controls.Presenters Vector newOffset = SnapOffset(new Vector(x, y)); bool offsetChanged = newOffset != Offset; - Offset = newOffset; + SetCurrentValue(OffsetProperty, newOffset); e.Handled = !IsScrollChainingEnabled || offsetChanged; } @@ -631,9 +667,14 @@ namespace Avalonia.Controls.Presenters protected override void OnPropertyChanged(AvaloniaPropertyChangedEventArgs change) { - if (change.Property == OffsetProperty && !_arranging) + if (change.Property == OffsetProperty) { - InvalidateArrange(); + if (!_arranging) + { + InvalidateArrange(); + } + + _owner?.SetCurrentValue(OffsetProperty, change.GetNewValue()); } else if (change.Property == ContentProperty) { @@ -651,11 +692,31 @@ namespace Avalonia.Controls.Presenters UpdateSnapPoints(); } + else if (change.Property == ChildProperty) + { + ChildChanged(change); + } else if (change.Property == HorizontalSnapPointsAlignmentProperty || change.Property == VerticalSnapPointsAlignmentProperty) { UpdateSnapPoints(); } + else if (change.Property == ExtentProperty) + { + if (_owner != null) + { + _owner.Extent = change.GetNewValue(); + } + CoerceValue(OffsetProperty); + } + else if (change.Property == ViewportProperty) + { + if (_owner != null) + { + _owner.Viewport = change.GetNewValue(); + } + CoerceValue(OffsetProperty); + } base.OnPropertyChanged(change); } @@ -677,7 +738,7 @@ namespace Avalonia.Controls.Presenters if (e.OldValue != null) { - Offset = default; + SetCurrentValue(OffsetProperty, default); } } @@ -719,14 +780,14 @@ namespace Avalonia.Controls.Presenters if (logicalScroll != scrollable.IsLogicalScrollEnabled) { UpdateScrollableSubscription(Child); - Offset = default; + SetCurrentValue(OffsetProperty, default); InvalidateMeasure(); } else if (scrollable.IsLogicalScrollEnabled) { Viewport = scrollable.Viewport; Extent = scrollable.Extent; - Offset = scrollable.Offset; + SetCurrentValue(OffsetProperty, scrollable.Offset); } } diff --git a/src/Avalonia.Controls/Primitives/HeaderedItemsControl.cs b/src/Avalonia.Controls/Primitives/HeaderedItemsControl.cs index 89f9968267..d71439ece6 100644 --- a/src/Avalonia.Controls/Primitives/HeaderedItemsControl.cs +++ b/src/Avalonia.Controls/Primitives/HeaderedItemsControl.cs @@ -13,7 +13,7 @@ namespace Avalonia.Controls.Primitives public class HeaderedItemsControl : ItemsControl, IContentPresenterHost { private IDisposable? _itemsBinding; - private bool _prepareItemContainerOnAttach; + private ItemsControl? _prepareItemContainerOnAttach; /// /// Defines the property. @@ -69,10 +69,10 @@ namespace Avalonia.Controls.Primitives { base.OnAttachedToLogicalTree(e); - if (_prepareItemContainerOnAttach) + if (_prepareItemContainerOnAttach is not null) { - PrepareItemContainer(); - _prepareItemContainerOnAttach = false; + PrepareItemContainer(_prepareItemContainerOnAttach); + _prepareItemContainerOnAttach = null; } } @@ -97,7 +97,7 @@ namespace Avalonia.Controls.Primitives return false; } - internal void PrepareItemContainer() + internal void PrepareItemContainer(ItemsControl parent) { _itemsBinding?.Dispose(); _itemsBinding = null; @@ -106,18 +106,18 @@ namespace Avalonia.Controls.Primitives if (item is null) { - _prepareItemContainerOnAttach = false; + _prepareItemContainerOnAttach = null; return; } - var headerTemplate = HeaderTemplate; + var headerTemplate = HeaderTemplate ?? parent.ItemTemplate; if (headerTemplate is null) { if (((ILogical)this).IsAttachedToLogicalTree) headerTemplate = this.FindDataTemplate(item); else - _prepareItemContainerOnAttach = true; + _prepareItemContainerOnAttach = parent; } if (headerTemplate is ITreeDataTemplate treeTemplate && diff --git a/src/Avalonia.Controls/Primitives/HeaderedSelectingItemsControl.cs b/src/Avalonia.Controls/Primitives/HeaderedSelectingItemsControl.cs index 49fc58c8f5..88ca1f1fe1 100644 --- a/src/Avalonia.Controls/Primitives/HeaderedSelectingItemsControl.cs +++ b/src/Avalonia.Controls/Primitives/HeaderedSelectingItemsControl.cs @@ -1,5 +1,8 @@ +using System; using Avalonia.Collections; using Avalonia.Controls.Presenters; +using Avalonia.Controls.Templates; +using Avalonia.Data; using Avalonia.LogicalTree; namespace Avalonia.Controls.Primitives @@ -9,12 +12,21 @@ namespace Avalonia.Controls.Primitives /// public class HeaderedSelectingItemsControl : SelectingItemsControl, IContentPresenterHost { + private IDisposable? _itemsBinding; + private ItemsControl? _prepareItemContainerOnAttach; + /// /// Defines the property. /// public static readonly StyledProperty HeaderProperty = HeaderedContentControl.HeaderProperty.AddOwner(); + /// + /// Defines the property. + /// + public static readonly StyledProperty HeaderTemplateProperty = + HeaderedItemsControl.HeaderTemplateProperty.AddOwner(); + /// /// Initializes static members of the class. /// @@ -32,6 +44,15 @@ namespace Avalonia.Controls.Primitives set { SetValue(HeaderProperty, value); } } + /// + /// Gets or sets the data template used to display the header content of the control. + /// + public IDataTemplate? HeaderTemplate + { + get => GetValue(HeaderTemplateProperty); + set => SetValue(HeaderTemplateProperty, value); + } + /// /// Gets the header presenter from the control's template. /// @@ -50,6 +71,17 @@ namespace Avalonia.Controls.Primitives return RegisterContentPresenter(presenter); } + protected override void OnAttachedToLogicalTree(LogicalTreeAttachmentEventArgs e) + { + base.OnAttachedToLogicalTree(e); + + if (_prepareItemContainerOnAttach is not null) + { + PrepareItemContainer(_prepareItemContainerOnAttach); + _prepareItemContainerOnAttach = null; + } + } + /// /// Called when an is registered with the control. /// @@ -65,6 +97,37 @@ namespace Avalonia.Controls.Primitives return false; } + internal void PrepareItemContainer(ItemsControl parent) + { + _itemsBinding?.Dispose(); + _itemsBinding = null; + + var item = Header; + + if (item is null) + { + _prepareItemContainerOnAttach = null; + return; + } + + var headerTemplate = HeaderTemplate ?? parent.ItemTemplate; + + if (headerTemplate is null) + { + if (((ILogical)this).IsAttachedToLogicalTree) + headerTemplate = this.FindDataTemplate(item); + else + _prepareItemContainerOnAttach = parent; + } + + if (headerTemplate is ITreeDataTemplate treeTemplate && + treeTemplate.Match(item) && + treeTemplate.ItemsSelector(item) is { } itemsBinding) + { + _itemsBinding = BindingOperations.Apply(this, ItemsSourceProperty, itemsBinding, null); + } + } + private void HeaderChanged(AvaloniaPropertyChangedEventArgs e) { if (e.OldValue is ILogical oldChild) diff --git a/src/Avalonia.Controls/Primitives/PopupRoot.cs b/src/Avalonia.Controls/Primitives/PopupRoot.cs index 952ba92e9b..a2034a2dbb 100644 --- a/src/Avalonia.Controls/Primitives/PopupRoot.cs +++ b/src/Avalonia.Controls/Primitives/PopupRoot.cs @@ -90,7 +90,6 @@ namespace Avalonia.Controls.Primitives public void Dispose() { PlatformImpl?.Dispose(); - HandleClosed(); } private void UpdatePosition() diff --git a/src/Avalonia.Controls/Primitives/RangeBase.cs b/src/Avalonia.Controls/Primitives/RangeBase.cs index 38d848d69b..fd9de47236 100644 --- a/src/Avalonia.Controls/Primitives/RangeBase.cs +++ b/src/Avalonia.Controls/Primitives/RangeBase.cs @@ -12,30 +12,22 @@ namespace Avalonia.Controls.Primitives /// /// Defines the property. /// - public static readonly DirectProperty MinimumProperty = - AvaloniaProperty.RegisterDirect( - nameof(Minimum), - o => o.Minimum, - (o, v) => o.Minimum = v); + public static readonly StyledProperty MinimumProperty = + AvaloniaProperty.Register(nameof(Minimum), coerce: CoerceMinimum); /// /// Defines the property. /// - public static readonly DirectProperty MaximumProperty = - AvaloniaProperty.RegisterDirect( - nameof(Maximum), - o => o.Maximum, - (o, v) => o.Maximum = v); + public static readonly StyledProperty MaximumProperty = + AvaloniaProperty.Register(nameof(Maximum), 100, coerce: CoerceMaximum); /// /// Defines the property. /// - public static readonly DirectProperty ValueProperty = - AvaloniaProperty.RegisterDirect( - nameof(Value), - o => o.Value, - (o, v) => o.Value = v, - defaultBindingMode: BindingMode.TwoWay); + public static readonly StyledProperty ValueProperty = + AvaloniaProperty.Register(nameof(Value), + defaultBindingMode: BindingMode.TwoWay, + coerce: CoerceValue); /// /// Defines the property. @@ -49,44 +41,26 @@ namespace Avalonia.Controls.Primitives public static readonly StyledProperty LargeChangeProperty = AvaloniaProperty.Register(nameof(LargeChange), 10); - private double _minimum; - private double _maximum = 100.0; - private double _value; - /// - /// Initializes a new instance of the class. + /// Gets or sets the minimum value. /// - public RangeBase() + public double Minimum { + get => GetValue(MinimumProperty); + set => SetValue(MinimumProperty, value); } - /// - /// Gets or sets the minimum value. - /// - public double Minimum + private static double CoerceMinimum(AvaloniaObject sender, double value) { - get - { - return _minimum; - } + return ValidateDouble(value) ? value : sender.GetValue(MinimumProperty); + } - set + private void OnMinimumChanged() + { + if (IsInitialized) { - if (!ValidateDouble(value)) - { - return; - } - - if (IsInitialized) - { - SetAndRaise(MinimumProperty, ref _minimum, value); - Maximum = ValidateMaximum(Maximum); - Value = ValidateValue(Value); - } - else - { - SetAndRaise(MinimumProperty, ref _minimum, value); - } + CoerceValue(MaximumProperty); + CoerceValue(ValueProperty); } } @@ -95,28 +69,22 @@ namespace Avalonia.Controls.Primitives /// public double Maximum { - get - { - return _maximum; - } + get => GetValue(MaximumProperty); + set => SetValue(MaximumProperty, value); + } + + private static double CoerceMaximum(AvaloniaObject sender, double value) + { + return ValidateDouble(value) + ? Math.Max(value, sender.GetValue(MinimumProperty)) + : sender.GetValue(MaximumProperty); + } - set + private void OnMaximumChanged() + { + if (IsInitialized) { - if (!ValidateDouble(value)) - { - return; - } - - if (IsInitialized) - { - value = ValidateMaximum(value); - SetAndRaise(MaximumProperty, ref _maximum, value); - Value = ValidateValue(Value); - } - else - { - SetAndRaise(MaximumProperty, ref _maximum, value); - } + CoerceValue(ValueProperty); } } @@ -125,28 +93,15 @@ namespace Avalonia.Controls.Primitives /// public double Value { - get - { - return _value; - } + get => GetValue(ValueProperty); + set => SetValue(ValueProperty, value); + } - set - { - if (!ValidateDouble(value)) - { - return; - } - - if (IsInitialized) - { - value = ValidateValue(value); - SetAndRaise(ValueProperty, ref _value, value); - } - else - { - SetAndRaise(ValueProperty, ref _value, value); - } - } + private static double CoerceValue(AvaloniaObject sender, double value) + { + return ValidateDouble(value) + ? MathUtilities.Clamp(value, sender.GetValue(MinimumProperty), sender.GetValue(MaximumProperty)) + : sender.GetValue(ValueProperty); } public double SmallChange @@ -165,37 +120,31 @@ namespace Avalonia.Controls.Primitives { base.OnInitialized(); - Maximum = ValidateMaximum(Maximum); - Value = ValidateValue(Value); + CoerceValue(MaximumProperty); + CoerceValue(ValueProperty); } - /// - /// Checks if the double value is not infinity nor NaN. - /// - /// The value. - private static bool ValidateDouble(double value) + protected override void OnPropertyChanged(AvaloniaPropertyChangedEventArgs change) { - return !double.IsInfinity(value) && !double.IsNaN(value); - } + base.OnPropertyChanged(change); - /// - /// Validates/coerces the property. - /// - /// The value. - /// The coerced value. - private double ValidateMaximum(double value) - { - return Math.Max(value, Minimum); + if (change.Property == MinimumProperty) + { + OnMinimumChanged(); + } + else if (change.Property == MaximumProperty) + { + OnMaximumChanged(); + } } /// - /// Validates/coerces the property. + /// Checks if the double value is not infinity nor NaN. /// /// The value. - /// The coerced value. - private double ValidateValue(double value) + private static bool ValidateDouble(double value) { - return MathUtilities.Clamp(value, Minimum, Maximum); + return !double.IsInfinity(value) && !double.IsNaN(value); } } } diff --git a/src/Avalonia.Controls/Primitives/ScrollBar.cs b/src/Avalonia.Controls/Primitives/ScrollBar.cs index b4a8408901..37aa1ebffd 100644 --- a/src/Avalonia.Controls/Primitives/ScrollBar.cs +++ b/src/Avalonia.Controls/Primitives/ScrollBar.cs @@ -6,6 +6,9 @@ using Avalonia.Layout; using Avalonia.Threading; using Avalonia.Controls.Metadata; using Avalonia.Automation.Peers; +using Avalonia.VisualTree; +using Avalonia.Reactive; +using System.Linq; namespace Avalonia.Controls.Primitives { @@ -80,6 +83,8 @@ namespace Avalonia.Controls.Primitives private Button? _pageDownButton; private DispatcherTimer? _timer; private bool _isExpanded; + private CompositeDisposable? _ownerSubscriptions; + private ScrollViewer? _owner; /// /// Initializes static members of the class. @@ -88,6 +93,8 @@ namespace Avalonia.Controls.Primitives { Thumb.DragDeltaEvent.AddClassHandler((x, e) => x.OnThumbDragDelta(e), RoutingStrategies.Bubble); Thumb.DragCompletedEvent.AddClassHandler((x, e) => x.OnThumbDragComplete(e), RoutingStrategies.Bubble); + + FocusableProperty.OverrideMetadata(new(false)); } /// @@ -178,9 +185,62 @@ namespace Avalonia.Controls.Primitives _ => throw new InvalidOperationException("Invalid value for ScrollBar.Visibility.") }; - SetValue(IsVisibleProperty, isVisible); + SetCurrentValue(IsVisibleProperty, isVisible); + } + + protected override void OnAttachedToVisualTree(VisualTreeAttachmentEventArgs e) + { + base.OnAttachedToVisualTree(e); + AttachToScrollViewer(); + } + + /// + /// Locates the first ancestor and binds to its properties. Properties which have been set through other means are not bound. + /// + /// + /// This method is automatically called when the control is attached to a visual tree. + /// + protected internal virtual void AttachToScrollViewer() + { + var owner = this.FindAncestorOfType(); + + if (owner == null) + { + _owner = null; + _ownerSubscriptions?.Dispose(); + _ownerSubscriptions = null; + return; + } + + if (owner == _owner) + { + return; + } + + _ownerSubscriptions?.Dispose(); + + var visibilitySource = Orientation == Orientation.Horizontal ? ScrollViewer.HorizontalScrollBarVisibilityProperty : ScrollViewer.VerticalScrollBarVisibilityProperty; + + var subscriptionDisposables = new IDisposable?[] + { + IfUnset(MaximumProperty, p => Bind(p, owner.GetObservable(ScrollViewer.ScrollBarMaximumProperty, ExtractOrdinate), BindingPriority.Template)), + IfUnset(ValueProperty, p => Bind(p, owner.GetObservable(ScrollViewer.OffsetProperty, ExtractOrdinate), BindingPriority.Template)), + IfUnset(ViewportSizeProperty, p => Bind(p, owner.GetObservable(ScrollViewer.ViewportProperty, ExtractOrdinate), BindingPriority.Template)), + IfUnset(VisibilityProperty, p => Bind(p, owner.GetObservable(visibilitySource), BindingPriority.Template)), + IfUnset(AllowAutoHideProperty, p => Bind(p, owner.GetObservable(ScrollViewer.AllowAutoHideProperty), BindingPriority.Template)), + IfUnset(LargeChangeProperty, p => Bind(p, owner.GetObservable(ScrollViewer.LargeChangeProperty).Select(ExtractOrdinate), BindingPriority.Template)), + IfUnset(SmallChangeProperty, p => Bind(p, owner.GetObservable(ScrollViewer.SmallChangeProperty).Select(ExtractOrdinate), BindingPriority.Template)) + }.Where(d => d != null).Cast().ToArray(); + + _owner = owner; + _ownerSubscriptions = new CompositeDisposable(subscriptionDisposables); + + IDisposable? IfUnset(T property, Func func) where T : AvaloniaProperty => IsSet(property) ? null : func(property); } + private double ExtractOrdinate(Vector v) => Orientation == Orientation.Horizontal ? v.X : v.Y; + private double ExtractOrdinate(Size v) => Orientation == Orientation.Horizontal ? v.Width : v.Height; + protected override void OnKeyDown(KeyEventArgs e) { if (e.Key == Key.PageUp) @@ -202,11 +262,20 @@ namespace Avalonia.Controls.Primitives if (change.Property == OrientationProperty) { UpdatePseudoClasses(change.GetNewValue()); + if (IsAttachedToVisualTree) + { + AttachToScrollViewer(); // there's no way to manually refresh bindings, so reapply them + } } else if (change.Property == AllowAutoHideProperty) { UpdateIsExpandedState(); } + else if (change.Property == ValueProperty) + { + var value = change.GetNewValue(); + _owner?.SetCurrentValue(ScrollViewer.OffsetProperty, Orientation == Orientation.Horizontal ? _owner.Offset.WithX(value) : _owner.Offset.WithY(value)); + } else { if (change.Property == MinimumProperty || @@ -373,25 +442,25 @@ namespace Avalonia.Controls.Primitives private void SmallDecrement() { - Value = Math.Max(Value - SmallChange, Minimum); + SetCurrentValue(ValueProperty, Math.Max(Value - SmallChange, Minimum)); OnScroll(ScrollEventType.SmallDecrement); } private void SmallIncrement() { - Value = Math.Min(Value + SmallChange, Maximum); + SetCurrentValue(ValueProperty, Math.Min(Value + SmallChange, Maximum)); OnScroll(ScrollEventType.SmallIncrement); } private void LargeDecrement() { - Value = Math.Max(Value - LargeChange, Minimum); + SetCurrentValue(ValueProperty, Math.Max(Value - LargeChange, Minimum)); OnScroll(ScrollEventType.LargeDecrement); } private void LargeIncrement() { - Value = Math.Min(Value + LargeChange, Maximum); + SetCurrentValue(ValueProperty, Math.Min(Value + LargeChange, Maximum)); OnScroll(ScrollEventType.LargeIncrement); } diff --git a/src/Avalonia.Controls/Primitives/SelectingItemsControl.cs b/src/Avalonia.Controls/Primitives/SelectingItemsControl.cs index 9c060f2258..663a315732 100644 --- a/src/Avalonia.Controls/Primitives/SelectingItemsControl.cs +++ b/src/Avalonia.Controls/Primitives/SelectingItemsControl.cs @@ -104,6 +104,14 @@ namespace Avalonia.Controls.Primitives AvaloniaProperty.Register( nameof(SelectionMode)); + /// + /// Defines the IsSelected attached property. + /// + public static readonly StyledProperty IsSelectedProperty = + AvaloniaProperty.RegisterAttached( + "IsSelected", + defaultBindingMode: BindingMode.TwoWay); + /// /// Defines the property. /// @@ -111,9 +119,8 @@ namespace Avalonia.Controls.Primitives AvaloniaProperty.Register(nameof(IsTextSearchEnabled), false); /// - /// Event that should be raised by items that implement to - /// notify the parent that their selection state - /// has changed. + /// Event that should be raised by containers when their selection state changes to notify + /// the parent that their selection state has changed. /// public static readonly RoutedEvent IsSelectedChangedEvent = RoutedEvent.Register( @@ -302,20 +309,9 @@ namespace Avalonia.Controls.Primitives { get { - if (_updateState?.Selection.HasValue == true) - { - return _updateState.Selection.Value; - } - else - { - if (_selection is null) - { - _selection = CreateDefaultSelectionModel(); - InitializeSelectionModel(_selection); - } - - return _selection; - } + return _updateState?.Selection.HasValue == true ? + _updateState.Selection.Value : + GetOrCreateSelectionModel(); } set { @@ -420,6 +416,21 @@ namespace Avalonia.Controls.Primitives /// The item. public void ScrollIntoView(object item) => ScrollIntoView(ItemsView.IndexOf(item)); + /// + /// Gets the value of the on the specified control. + /// + /// The control. + /// The value of the attached property. + public static bool GetIsSelected(Control control) => control.GetValue(IsSelectedProperty); + + /// + /// Gets the value of the on the specified control. + /// + /// The control. + /// The value of the property. + /// The value of the attached property. + public static void SetIsSelected(Control control, bool value) => control.SetValue(IsSelectedProperty, value); + /// /// Tries to get the container that was the source of an event. /// @@ -473,20 +484,36 @@ namespace Avalonia.Controls.Primitives } } - /// - protected internal override void PrepareContainerForItemOverride(Control element, object? item, int index) + protected internal override void PrepareContainerForItemOverride(Control container, object? item, int index) + { + // Ensure that the selection model is created at this point so that accessing it in + // ContainerForItemPreparedOverride doesn't cause it to be initialized (which can + // make containers become deselected when they're synced with the empty selection + // mode). + GetOrCreateSelectionModel(); + + base.PrepareContainerForItemOverride(container, item, index); + } + + protected internal override void ContainerForItemPreparedOverride(Control container, object? item, int index) { - base.PrepareContainerForItemOverride(element, item, index); + base.ContainerForItemPreparedOverride(container, item, index); - if ((element as ISelectable)?.IsSelected == true) + // Once the container has been full prepared and added to the tree, any bindings from + // styles or item container themes are guaranteed to be applied. + if (!container.IsSet(IsSelectedProperty)) { - Selection.Select(index); - MarkContainerSelected(element, true); + // The IsSelected property is not set on the container: update the container + // selection based on the current selection as understood by this control. + MarkContainerSelected(container, Selection.IsSelected(index)); } else { - var selected = Selection.IsSelected(index); - MarkContainerSelected(element, selected); + // The IsSelected property is set on the container: there is a style or item + // container theme which has bound the IsSelected property. Update our selection + // based on the selection state of the container. + var containerIsSelected = GetIsSelected(container); + UpdateSelection(index, containerIsSelected, toggleModifier: true); } } @@ -508,8 +535,7 @@ namespace Avalonia.Controls.Primitives KeyboardNavigation.SetTabOnceActiveElement(panel, null); } - if (element is ISelectable) - MarkContainerSelected(element, false); + element.ClearValue(IsSelectedProperty); } /// @@ -874,6 +900,17 @@ namespace Avalonia.Controls.Primitives return false; } + private ISelectionModel GetOrCreateSelectionModel() + { + if (_selection is null) + { + _selection = CreateDefaultSelectionModel(); + InitializeSelectionModel(_selection); + } + + return _selection; + } + private void OnItemsViewSourceChanged(object? sender, EventArgs e) { if (_selection is not null && _updateState is null) @@ -1098,11 +1135,14 @@ namespace Avalonia.Controls.Primitives { if (!_ignoreContainerSelectionChanged && e.Source is Control control && - e.Source is ISelectable selectable && control.Parent == this && - IndexFromContainer(control) != -1) + IndexFromContainer(control) is var index && + index >= 0) { - UpdateSelection(control, selectable.IsSelected); + if (GetIsSelected(control)) + Selection.Select(index); + else + Selection.Deselect(index); } if (e.Source != this) @@ -1112,31 +1152,18 @@ namespace Avalonia.Controls.Primitives } /// - /// Sets a container's 'selected' class or . + /// Sets the on the specified container. /// /// The container. /// Whether the control is selected /// The previous selection state. - private bool MarkContainerSelected(Control container, bool selected) + private void MarkContainerSelected(Control container, bool selected) { + _ignoreContainerSelectionChanged = true; + try { - bool result; - - _ignoreContainerSelectionChanged = true; - - if (container is ISelectable selectable) - { - result = selectable.IsSelected; - selectable.IsSelected = selected; - } - else - { - result = container.Classes.Contains(":selected"); - ((IPseudoClasses)container.Classes).Set(":selected", selected); - } - - return result; + container.SetCurrentValue(IsSelectedProperty, selected); } finally { diff --git a/src/Avalonia.Controls/Primitives/Thumb.cs b/src/Avalonia.Controls/Primitives/Thumb.cs index cb3195cf52..c205830bc2 100644 --- a/src/Avalonia.Controls/Primitives/Thumb.cs +++ b/src/Avalonia.Controls/Primitives/Thumb.cs @@ -2,6 +2,7 @@ using System; using Avalonia.Controls.Metadata; using Avalonia.Input; using Avalonia.Interactivity; +using Avalonia.VisualTree; namespace Avalonia.Controls.Primitives { @@ -80,20 +81,22 @@ namespace Avalonia.Controls.Primitives { if (_lastPoint.HasValue) { + var point = e.GetPosition(this.GetVisualParent()); var ev = new VectorEventArgs { RoutedEvent = DragDeltaEvent, - Vector = e.GetPosition(this) - _lastPoint.Value, + Vector = point - _lastPoint.Value, }; RaiseEvent(ev); + _lastPoint = point; } } protected override void OnPointerPressed(PointerPressedEventArgs e) { e.Handled = true; - _lastPoint = e.GetPosition(this); + _lastPoint = e.GetPosition(this.GetVisualParent()); var ev = new VectorEventArgs { @@ -116,7 +119,7 @@ namespace Avalonia.Controls.Primitives var ev = new VectorEventArgs { RoutedEvent = DragCompletedEvent, - Vector = (Vector)e.GetPosition(this), + Vector = (Vector)e.GetPosition(this.GetVisualParent()), }; RaiseEvent(ev); diff --git a/src/Avalonia.Controls/Primitives/Track.cs b/src/Avalonia.Controls/Primitives/Track.cs index 9e8d1478fa..fe4912a33c 100644 --- a/src/Avalonia.Controls/Primitives/Track.cs +++ b/src/Avalonia.Controls/Primitives/Track.cs @@ -15,14 +15,14 @@ namespace Avalonia.Controls.Primitives [PseudoClasses(":vertical", ":horizontal")] public class Track : Control { - public static readonly DirectProperty MinimumProperty = - RangeBase.MinimumProperty.AddOwner(o => o.Minimum, (o, v) => o.Minimum = v); + public static readonly StyledProperty MinimumProperty = + RangeBase.MinimumProperty.AddOwner(); - public static readonly DirectProperty MaximumProperty = - RangeBase.MaximumProperty.AddOwner(o => o.Maximum, (o, v) => o.Maximum = v); + public static readonly StyledProperty MaximumProperty = + RangeBase.MaximumProperty.AddOwner(); - public static readonly DirectProperty ValueProperty = - RangeBase.ValueProperty.AddOwner(o => o.Value, (o, v) => o.Value = v); + public static readonly StyledProperty ValueProperty = + RangeBase.ValueProperty.AddOwner(); public static readonly StyledProperty ViewportSizeProperty = ScrollBar.ViewportSizeProperty.AddOwner(); @@ -45,10 +45,6 @@ namespace Avalonia.Controls.Primitives public static readonly StyledProperty IgnoreThumbDragProperty = AvaloniaProperty.Register(nameof(IgnoreThumbDrag)); - private double _minimum; - private double _maximum = 100.0; - private double _value; - static Track() { ThumbProperty.Changed.AddClassHandler((x, e) => x.ThumbChanged(e)); @@ -64,20 +60,20 @@ namespace Avalonia.Controls.Primitives public double Minimum { - get { return _minimum; } - set { SetAndRaise(MinimumProperty, ref _minimum, value); } + get => GetValue(MinimumProperty); + set => SetValue(MinimumProperty, value); } public double Maximum { - get { return _maximum; } - set { SetAndRaise(MaximumProperty, ref _maximum, value); } + get => GetValue(MaximumProperty); + set => SetValue(MaximumProperty, value); } public double Value { - get { return _value; } - set { SetAndRaise(ValueProperty, ref _value, value); } + get => GetValue(ValueProperty); + set => SetValue(ValueProperty, value); } public double ViewportSize @@ -443,11 +439,11 @@ namespace Avalonia.Controls.Primitives { if (IgnoreThumbDrag) return; - - Value = MathUtilities.Clamp( + + SetCurrentValue(ValueProperty, MathUtilities.Clamp( Value + ValueFromDistance(e.Vector.X, e.Vector.Y), Minimum, - Maximum); + Maximum)); } private void ShowChildren(bool visible) diff --git a/src/Avalonia.Controls/ProgressBar.cs b/src/Avalonia.Controls/ProgressBar.cs index 98a9ec60bf..daf6be12d2 100644 --- a/src/Avalonia.Controls/ProgressBar.cs +++ b/src/Avalonia.Controls/ProgressBar.cs @@ -100,8 +100,6 @@ namespace Avalonia.Controls } private double _percentage; - private double _indeterminateStartingOffset; - private double _indeterminateEndingOffset; private Border? _indicator; private IDisposable? _trackSizeChangedListener; @@ -122,17 +120,11 @@ namespace Avalonia.Controls nameof(Percentage), o => o.Percentage); - public static readonly DirectProperty IndeterminateStartingOffsetProperty = - AvaloniaProperty.RegisterDirect( - nameof(IndeterminateStartingOffset), - p => p.IndeterminateStartingOffset, - (p, o) => p.IndeterminateStartingOffset = o); + public static readonly StyledProperty IndeterminateStartingOffsetProperty = + AvaloniaProperty.Register(nameof(IndeterminateStartingOffset)); - public static readonly DirectProperty IndeterminateEndingOffsetProperty = - AvaloniaProperty.RegisterDirect( - nameof(IndeterminateEndingOffset), - p => p.IndeterminateEndingOffset, - (p, o) => p.IndeterminateEndingOffset = o); + public static readonly StyledProperty IndeterminateEndingOffsetProperty = + AvaloniaProperty.Register(nameof(IndeterminateEndingOffset)); public double Percentage { @@ -142,19 +134,19 @@ namespace Avalonia.Controls public double IndeterminateStartingOffset { - get => _indeterminateStartingOffset; - set => SetAndRaise(IndeterminateStartingOffsetProperty, ref _indeterminateStartingOffset, value); + get => GetValue(IndeterminateStartingOffsetProperty); + set => SetValue(IndeterminateStartingOffsetProperty, value); } public double IndeterminateEndingOffset { - get => _indeterminateEndingOffset; - set => SetAndRaise(IndeterminateEndingOffsetProperty, ref _indeterminateEndingOffset, value); + get => GetValue(IndeterminateEndingOffsetProperty); + set => SetValue(IndeterminateEndingOffsetProperty, value); } static ProgressBar() { - ValueProperty.OverrideMetadata(new DirectPropertyMetadata(defaultBindingMode: BindingMode.OneWay)); + ValueProperty.OverrideMetadata(new(defaultBindingMode: BindingMode.OneWay)); ValueProperty.Changed.AddClassHandler((x, e) => x.UpdateIndicatorWhenPropChanged(e)); MinimumProperty.Changed.AddClassHandler((x, e) => x.UpdateIndicatorWhenPropChanged(e)); MaximumProperty.Changed.AddClassHandler((x, e) => x.UpdateIndicatorWhenPropChanged(e)); @@ -261,8 +253,8 @@ namespace Avalonia.Controls // Remove these properties when we switch to fluent as default and removed the old one. - IndeterminateStartingOffset = -dim; - IndeterminateEndingOffset = dim; + SetCurrentValue(IndeterminateStartingOffsetProperty,-dim); + SetCurrentValue(IndeterminateEndingOffsetProperty,dim); var padding = Padding; var rectangle = new RectangleGeometry( diff --git a/src/Avalonia.Controls/ScrollViewer.cs b/src/Avalonia.Controls/ScrollViewer.cs index 0f1b8f388c..a7188c6226 100644 --- a/src/Avalonia.Controls/ScrollViewer.cs +++ b/src/Avalonia.Controls/ScrollViewer.cs @@ -16,54 +16,25 @@ namespace Avalonia.Controls [TemplatePart("PART_VerticalScrollBar", typeof(ScrollBar))] public class ScrollViewer : ContentControl, IScrollable, IScrollAnchorProvider { - /// - /// Defines the property. - /// - /// - /// There is no public C# accessor for this property as it is intended to be bound to by a - /// in the control's template. - /// - public static readonly DirectProperty CanHorizontallyScrollProperty = - AvaloniaProperty.RegisterDirect( - nameof(CanHorizontallyScroll), - o => o.CanHorizontallyScroll); - - /// - /// Defines the property. - /// - /// - /// There is no public C# accessor for this property as it is intended to be bound to by a - /// in the control's template. - /// - public static readonly DirectProperty CanVerticallyScrollProperty = - AvaloniaProperty.RegisterDirect( - nameof(CanVerticallyScroll), - o => o.CanVerticallyScroll); - /// /// Defines the property. /// public static readonly DirectProperty ExtentProperty = AvaloniaProperty.RegisterDirect(nameof(Extent), - o => o.Extent, - (o, v) => o.Extent = v); + o => o.Extent); /// /// Defines the property. /// - public static readonly DirectProperty OffsetProperty = - AvaloniaProperty.RegisterDirect( - nameof(Offset), - o => o.Offset, - (o, v) => o.Offset = v); + public static readonly StyledProperty OffsetProperty = + AvaloniaProperty.Register(nameof(Offset), coerce: CoerceOffset); /// /// Defines the property. /// public static readonly DirectProperty ViewportProperty = AvaloniaProperty.RegisterDirect(nameof(Viewport), - o => o.Viewport, - (o, v) => o.Viewport = v); + o => o.Viewport); /// /// Defines the property. @@ -82,41 +53,12 @@ namespace Avalonia.Controls o => o.SmallChange); /// - /// Defines the HorizontalScrollBarMaximum property. - /// - /// - /// There is no public C# accessor for this property as it is intended to be bound to by a - /// in the control's template. - /// - public static readonly DirectProperty HorizontalScrollBarMaximumProperty = - AvaloniaProperty.RegisterDirect( - nameof(HorizontalScrollBarMaximum), - o => o.HorizontalScrollBarMaximum); - - /// - /// Defines the HorizontalScrollBarValue property. + /// Defines the property. /// - /// - /// There is no public C# accessor for this property as it is intended to be bound to by a - /// in the control's template. - /// - public static readonly DirectProperty HorizontalScrollBarValueProperty = - AvaloniaProperty.RegisterDirect( - nameof(HorizontalScrollBarValue), - o => o.HorizontalScrollBarValue, - (o, v) => o.HorizontalScrollBarValue = v); - - /// - /// Defines the HorizontalScrollBarViewportSize property. - /// - /// - /// There is no public C# accessor for this property as it is intended to be bound to by a - /// in the control's template. - /// - public static readonly DirectProperty HorizontalScrollBarViewportSizeProperty = - AvaloniaProperty.RegisterDirect( - nameof(HorizontalScrollBarViewportSize), - o => o.HorizontalScrollBarViewportSize); + public static readonly DirectProperty ScrollBarMaximumProperty = + AvaloniaProperty.RegisterDirect( + nameof(ScrollBarMaximum), + o => o.ScrollBarMaximum); /// /// Defines the property. @@ -126,31 +68,6 @@ namespace Avalonia.Controls nameof(HorizontalScrollBarVisibility), ScrollBarVisibility.Disabled); - /// - /// Defines the VerticalScrollBarMaximum property. - /// - /// - /// There is no public C# accessor for this property as it is intended to be bound to by a - /// in the control's template. - /// - public static readonly DirectProperty VerticalScrollBarMaximumProperty = - AvaloniaProperty.RegisterDirect( - nameof(VerticalScrollBarMaximum), - o => o.VerticalScrollBarMaximum); - - /// - /// Defines the VerticalScrollBarValue property. - /// - /// - /// There is no public C# accessor for this property as it is intended to be bound to by a - /// in the control's template. - /// - public static readonly DirectProperty VerticalScrollBarValueProperty = - AvaloniaProperty.RegisterDirect( - nameof(VerticalScrollBarValue), - o => o.VerticalScrollBarValue, - (o, v) => o.VerticalScrollBarValue = v); - /// /// Defines the property. /// @@ -179,18 +96,6 @@ namespace Avalonia.Controls AvaloniaProperty.RegisterAttached( nameof(VerticalSnapPointsAlignment)); - /// - /// Defines the VerticalScrollBarViewportSize property. - /// - /// - /// There is no public C# accessor for this property as it is intended to be bound to by a - /// in the control's template. - /// - public static readonly DirectProperty VerticalScrollBarViewportSizeProperty = - AvaloniaProperty.RegisterDirect( - nameof(VerticalScrollBarViewportSize), - o => o.VerticalScrollBarViewportSize); - /// /// Defines the property. /// @@ -242,25 +147,16 @@ namespace Avalonia.Controls private IDisposable? _childSubscription; private ILogicalScrollable? _logicalScrollable; private Size _extent; - private Vector _offset; private Size _viewport; private Size _oldExtent; private Vector _oldOffset; + private Vector _oldMaximum; private Size _oldViewport; private Size _largeChange; private Size _smallChange = new Size(DefaultSmallChange, DefaultSmallChange); private bool _isExpanded; private IDisposable? _scrollBarExpandSubscription; - /// - /// Initializes static members of the class. - /// - static ScrollViewer() - { - HorizontalScrollBarVisibilityProperty.Changed.AddClassHandler((x, e) => x.ScrollBarVisibilityChanged(e)); - VerticalScrollBarVisibilityProperty.Changed.AddClassHandler((x, e) => x.ScrollBarVisibilityChanged(e)); - } - /// /// Initializes a new instance of the class. /// @@ -288,7 +184,7 @@ namespace Avalonia.Controls return _extent; } - private set + internal set { if (SetAndRaise(ExtentProperty, ref _extent, value)) { @@ -302,18 +198,8 @@ namespace Avalonia.Controls /// public Vector Offset { - get - { - return _offset; - } - - set - { - if (SetAndRaise(OffsetProperty, ref _offset, CoerceOffset(Extent, Viewport, value))) - { - CalculatedPropertiesChanged(); - } - } + get => GetValue(OffsetProperty); + set => SetValue(OffsetProperty, value); } /// @@ -326,7 +212,7 @@ namespace Avalonia.Controls return _viewport; } - private set + internal set { if (SetAndRaise(ViewportProperty, ref _viewport, value)) { @@ -383,70 +269,9 @@ namespace Avalonia.Controls public Control? CurrentAnchor => (Presenter as IScrollAnchorProvider)?.CurrentAnchor; /// - /// Gets the maximum horizontal scrollbar value. - /// - protected double HorizontalScrollBarMaximum - { - get { return Max(_extent.Width - _viewport.Width, 0); } - } - - /// - /// Gets or sets the horizontal scrollbar value. + /// Gets the maximum scrolling distance (which is - ). /// - protected double HorizontalScrollBarValue - { - get { return _offset.X; } - set - { - if (_offset.X != value) - { - var old = Offset.X; - Offset = Offset.WithX(value); - RaisePropertyChanged(HorizontalScrollBarValueProperty, old, value); - } - } - } - - /// - /// Gets the size of the horizontal scrollbar viewport. - /// - protected double HorizontalScrollBarViewportSize - { - get { return _viewport.Width; } - } - - /// - /// Gets the maximum vertical scrollbar value. - /// - protected double VerticalScrollBarMaximum - { - get { return Max(_extent.Height - _viewport.Height, 0); } - } - - /// - /// Gets or sets the vertical scrollbar value. - /// - protected double VerticalScrollBarValue - { - get { return _offset.Y; } - set - { - if (_offset.Y != value) - { - var old = Offset.Y; - Offset = Offset.WithY(value); - RaisePropertyChanged(VerticalScrollBarValueProperty, old, value); - } - } - } - - /// - /// Gets the size of the vertical scrollbar viewport. - /// - protected double VerticalScrollBarViewportSize - { - get { return _viewport.Height; } - } + public Vector ScrollBarMaximum => new(Max(_extent.Width - _viewport.Width, 0), Max(_extent.Height - _viewport.Height, 0)); /// /// Gets a value that indicates whether any scrollbar is expanded. @@ -528,82 +353,52 @@ namespace Avalonia.Controls /// /// Scrolls the content up one line. /// - public void LineUp() - { - Offset -= new Vector(0, _smallChange.Height); - } + public void LineUp() => SetCurrentValue(OffsetProperty, Offset - new Vector(0, _smallChange.Height)); /// /// Scrolls the content down one line. /// - public void LineDown() - { - Offset += new Vector(0, _smallChange.Height); - } + public void LineDown() => SetCurrentValue(OffsetProperty, Offset + new Vector(0, _smallChange.Height)); /// /// Scrolls the content left one line. /// - public void LineLeft() - { - Offset -= new Vector(_smallChange.Width, 0); - } + public void LineLeft() => SetCurrentValue(OffsetProperty, Offset - new Vector(_smallChange.Width, 0)); /// /// Scrolls the content right one line. /// - public void LineRight() - { - Offset += new Vector(_smallChange.Width, 0); - } + public void LineRight() => SetCurrentValue(OffsetProperty, Offset + new Vector(_smallChange.Width, 0)); /// /// Scrolls the content upward by one page. /// - public void PageUp() - { - VerticalScrollBarValue = Math.Max(_offset.Y - _viewport.Height, 0); - } + public void PageUp() => SetCurrentValue(OffsetProperty, Offset.WithY(Math.Max(Offset.Y - _viewport.Height, 0))); /// /// Scrolls the content downward by one page. /// - public void PageDown() - { - VerticalScrollBarValue = Math.Min(_offset.Y + _viewport.Height, VerticalScrollBarMaximum); - } + public void PageDown() => SetCurrentValue(OffsetProperty, Offset.WithY(Math.Min(Offset.Y + _viewport.Height, ScrollBarMaximum.Y))); /// /// Scrolls the content left by one page. /// - public void PageLeft() - { - HorizontalScrollBarValue = Math.Max(_offset.X - _viewport.Width, 0); - } + public void PageLeft() => SetCurrentValue(OffsetProperty, Offset.WithX(Math.Max(Offset.X - _viewport.Width, 0))); /// /// Scrolls the content tight by one page. /// - public void PageRight() - { - HorizontalScrollBarValue = Math.Min(_offset.X + _viewport.Width, HorizontalScrollBarMaximum); - } + public void PageRight() => SetCurrentValue(OffsetProperty, Offset.WithX(Math.Min(Offset.X + _viewport.Width, ScrollBarMaximum.X))); /// /// Scrolls to the top-left corner of the content. /// - public void ScrollToHome() - { - Offset = new Vector(double.NegativeInfinity, double.NegativeInfinity); - } + public void ScrollToHome() => SetCurrentValue(OffsetProperty, new Vector(double.NegativeInfinity, double.NegativeInfinity)); /// /// Scrolls to the bottom-left corner of the content. /// - public void ScrollToEnd() - { - Offset = new Vector(double.NegativeInfinity, double.PositiveInfinity); - } + public void ScrollToEnd() => SetCurrentValue(OffsetProperty, new Vector(double.NegativeInfinity, double.PositiveInfinity)); /// /// Gets the value of the HorizontalScrollBarVisibility attached property. @@ -819,11 +614,14 @@ namespace Avalonia.Controls return false; } - internal static Vector CoerceOffset(Size extent, Size viewport, Vector offset) + internal static Vector CoerceOffset(AvaloniaObject sender, Vector value) { + var extent = sender.GetValue(ExtentProperty); + var viewport = sender.GetValue(ViewportProperty); + var maxX = Math.Max(extent.Width - viewport.Width, 0); var maxY = Math.Max(extent.Height - viewport.Height, 0); - return new Vector(Clamp(offset.X, 0, maxX), Clamp(offset.Y, 0, maxY)); + return new Vector(Clamp(value.X, 0, maxX), Clamp(value.Y, 0, maxY)); } private static double Clamp(double value, double min, double max) @@ -859,40 +657,14 @@ namespace Avalonia.Controls CalculatedPropertiesChanged(); } - private void ScrollBarVisibilityChanged(AvaloniaPropertyChangedEventArgs e) + private void CalculatedPropertiesChanged() { - var wasEnabled = e.OldValue.GetValueOrDefault() != ScrollBarVisibility.Disabled; - var isEnabled = e.NewValue.GetValueOrDefault() != ScrollBarVisibility.Disabled; - - if (wasEnabled != isEnabled) + var newMaximum = ScrollBarMaximum; + if (newMaximum != _oldMaximum) { - if (e.Property == HorizontalScrollBarVisibilityProperty) - { - RaisePropertyChanged( - CanHorizontallyScrollProperty, - wasEnabled, - isEnabled); - } - else if (e.Property == VerticalScrollBarVisibilityProperty) - { - RaisePropertyChanged( - CanVerticallyScrollProperty, - wasEnabled, - isEnabled); - } + RaisePropertyChanged(ScrollBarMaximumProperty, _oldMaximum, newMaximum); + _oldMaximum = newMaximum; } - } - - private void CalculatedPropertiesChanged() - { - // Pass old values of 0 here because we don't have the old values at this point, - // and it shouldn't matter as only the template uses these properties. - RaisePropertyChanged(HorizontalScrollBarMaximumProperty, 0, HorizontalScrollBarMaximum); - RaisePropertyChanged(HorizontalScrollBarValueProperty, 0, HorizontalScrollBarValue); - RaisePropertyChanged(HorizontalScrollBarViewportSizeProperty, 0, HorizontalScrollBarViewportSize); - RaisePropertyChanged(VerticalScrollBarMaximumProperty, 0, VerticalScrollBarMaximum); - RaisePropertyChanged(VerticalScrollBarValueProperty, 0, VerticalScrollBarValue); - RaisePropertyChanged(VerticalScrollBarViewportSizeProperty, 0, VerticalScrollBarViewportSize); if (_logicalScrollable?.IsLogicalScrollEnabled == true) { @@ -906,6 +678,24 @@ namespace Avalonia.Controls } } + protected override void OnPropertyChanged(AvaloniaPropertyChangedEventArgs change) + { + base.OnPropertyChanged(change); + + if (change.Property == OffsetProperty) + { + CalculatedPropertiesChanged(); + } + else if (change.Property == ExtentProperty) + { + CoerceValue(OffsetProperty); + } + else if (change.Property == ViewportProperty) + { + CoerceValue(OffsetProperty); + } + } + protected override void OnKeyDown(KeyEventArgs e) { if (e.Key == Key.PageUp) diff --git a/src/Avalonia.Controls/SelectableTextBlock.cs b/src/Avalonia.Controls/SelectableTextBlock.cs index 60b0a77f30..5d14611a62 100644 --- a/src/Avalonia.Controls/SelectableTextBlock.cs +++ b/src/Avalonia.Controls/SelectableTextBlock.cs @@ -19,10 +19,10 @@ namespace Avalonia.Controls public class SelectableTextBlock : TextBlock, IInlineHost { public static readonly StyledProperty SelectionStartProperty = - TextBox.SelectionStartProperty.AddOwner(new(coerce: TextBox.CoerceCaretIndex)); + TextBox.SelectionStartProperty.AddOwner(); public static readonly StyledProperty SelectionEndProperty = - TextBox.SelectionEndProperty.AddOwner(new(coerce: TextBox.CoerceCaretIndex)); + TextBox.SelectionEndProperty.AddOwner(); public static readonly DirectProperty SelectedTextProperty = AvaloniaProperty.RegisterDirect( @@ -231,7 +231,7 @@ namespace Avalonia.Controls { base.OnPointerPressed(e); - var text = Text; + var text = HasComplexContent ? Inlines?.Text : Text; var clickInfo = e.GetCurrentPoint(this); if (text != null && clickInfo.Properties.IsLeftButtonPressed) @@ -317,7 +317,7 @@ namespace Avalonia.Controls // selection should not change during pointer move if the user right clicks if (e.Pointer.Captured == this && e.GetCurrentPoint(this).Properties.IsLeftButtonPressed) { - var text = Text; + var text = HasComplexContent ? Inlines?.Text : Text; var padding = Padding; var point = e.GetPosition(this) - new Point(padding.Left, padding.Top); @@ -400,7 +400,10 @@ namespace Avalonia.Controls private string GetSelection() { - var textLength = Text?.Length ?? 0; + var text = HasComplexContent ? Inlines?.Text : Text; + + var textLength = text?.Length ?? 0; + if (textLength == 0) { return ""; @@ -418,7 +421,7 @@ namespace Avalonia.Controls var length = Math.Max(0, end - start); - var selectedText = Text!.Substring(start, length); + var selectedText = text!.Substring(start, length); return selectedText; } diff --git a/src/Avalonia.Controls/Slider.cs b/src/Avalonia.Controls/Slider.cs index 7e43f6682c..b0dff5be79 100644 --- a/src/Avalonia.Controls/Slider.cs +++ b/src/Avalonia.Controls/Slider.cs @@ -110,7 +110,7 @@ namespace Avalonia.Controls Thumb.DragCompletedEvent.AddClassHandler((x, e) => x.OnThumbDragCompleted(e), RoutingStrategies.Bubble); - ValueProperty.OverrideMetadata(new DirectPropertyMetadata(enableDataValidation: true)); + ValueProperty.OverrideMetadata(new(enableDataValidation: true)); AutomationProperties.ControlTypeOverrideProperty.OverrideDefaultValue(AutomationControlType.Slider); } @@ -246,11 +246,11 @@ namespace Avalonia.Controls break; case Key.Home: - Value = Minimum; + SetCurrentValue(ValueProperty, Minimum); break; case Key.End: - Value = Maximum; + SetCurrentValue(ValueProperty, Maximum); break; default: @@ -313,7 +313,7 @@ namespace Avalonia.Controls // Update if we've found a better value if (Math.Abs(next - value) > Tolerance) { - Value = next; + SetCurrentValue(ValueProperty, next); } } @@ -366,7 +366,7 @@ namespace Avalonia.Controls var range = Maximum - Minimum; var finalValue = calcVal * range + Minimum; - Value = IsSnapToTickEnabled ? SnapToTick(finalValue) : finalValue; + SetCurrentValue(ValueProperty, IsSnapToTickEnabled ? SnapToTick(finalValue) : finalValue); } /// diff --git a/src/Avalonia.Controls/SplitButton/ToggleSplitButton.cs b/src/Avalonia.Controls/SplitButton/ToggleSplitButton.cs index 70d2983b9b..509b58833f 100644 --- a/src/Avalonia.Controls/SplitButton/ToggleSplitButton.cs +++ b/src/Avalonia.Controls/SplitButton/ToggleSplitButton.cs @@ -70,7 +70,7 @@ namespace Avalonia.Controls /// protected void Toggle() { - IsChecked = !IsChecked; + SetCurrentValue(IsCheckedProperty, !IsChecked); } /// diff --git a/src/Avalonia.Controls/SystemDialog.cs b/src/Avalonia.Controls/SystemDialog.cs index ee41b94b5a..d2b893df37 100644 --- a/src/Avalonia.Controls/SystemDialog.cs +++ b/src/Avalonia.Controls/SystemDialog.cs @@ -18,7 +18,7 @@ namespace Avalonia.Controls /// Gets or sets a collection of filters which determine the types of files displayed in an /// or an . /// - public List? Filters { get; set; } = new List(); + public List Filters { get; set; } = new List(); /// /// Gets or sets initial file name that is displayed when the dialog is opened. diff --git a/src/Avalonia.Controls/TabItem.cs b/src/Avalonia.Controls/TabItem.cs index 76680c0420..46265fb5bc 100644 --- a/src/Avalonia.Controls/TabItem.cs +++ b/src/Avalonia.Controls/TabItem.cs @@ -22,7 +22,7 @@ namespace Avalonia.Controls /// Defines the property. /// public static readonly StyledProperty IsSelectedProperty = - ListBoxItem.IsSelectedProperty.AddOwner(); + SelectingItemsControl.IsSelectedProperty.AddOwner(); /// /// Initializes static members of the class. diff --git a/src/Avalonia.Controls/TextBlock.cs b/src/Avalonia.Controls/TextBlock.cs index b55ae74965..19eaaaa0d9 100644 --- a/src/Avalonia.Controls/TextBlock.cs +++ b/src/Avalonia.Controls/TextBlock.cs @@ -663,7 +663,6 @@ namespace Avalonia.Controls var padding = LayoutHelper.RoundLayoutThickness(Padding, scale, scale); _constraint = availableSize.Deflate(padding); - _textLayout?.Dispose(); _textLayout = null; @@ -690,18 +689,18 @@ namespace Avalonia.Controls inline.BuildTextRun(textRuns); } - foreach (var textRun in textRuns) + _textRuns = textRuns; + + foreach (var textRun in _textRuns) { if (textRun is EmbeddedControlRun controlRun && - controlRun.Control is Control control) + controlRun.Control is Control control) { VisualChildren.Add(control); control.Measure(Size.Infinity); } } - - _textRuns = textRuns; } var measuredSize = TextLayout.Bounds.Size.Inflate(padding); @@ -711,64 +710,39 @@ namespace Avalonia.Controls protected override Size ArrangeOverride(Size finalSize) { - var textWidth = Math.Ceiling(TextLayout.Bounds.Width); - - if (finalSize.Width < textWidth) - { - finalSize = finalSize.WithWidth(textWidth); - } - - var scale = LayoutHelper.GetLayoutScale(this); - - var padding = LayoutHelper.RoundLayoutThickness(Padding, scale, scale); - if (HasComplexContent) { - ArrangeComplexContent(TextLayout, padding); - } + var scale = LayoutHelper.GetLayoutScale(this); - if (MathUtilities.AreClose(_constraint.Inflate(padding).Width, finalSize.Width)) - { - return finalSize; - } + var padding = LayoutHelper.RoundLayoutThickness(Padding, scale, scale); - _constraint = new Size(Math.Ceiling(finalSize.Deflate(padding).Width), double.PositiveInfinity); + var currentY = padding.Top; - _textLayout?.Dispose(); - _textLayout = null; - - if (HasComplexContent) - { - ArrangeComplexContent(TextLayout, padding); - } - - return finalSize; - } - - private static void ArrangeComplexContent(TextLayout textLayout, Thickness padding) - { - var currentY = padding.Top; - - foreach (var textLine in textLayout.TextLines) - { - var currentX = padding.Left + textLine.Start; - - foreach (var run in textLine.TextRuns) + foreach (var textLine in TextLayout.TextLines) { - if (run is DrawableTextRun drawable) + var currentX = padding.Left + textLine.Start; + + foreach (var run in textLine.TextRuns) { - if (drawable is EmbeddedControlRun controlRun - && controlRun.Control is Control control) + if (run is DrawableTextRun drawable) { - control.Arrange(new Rect(new Point(currentX, currentY), control.DesiredSize)); + if (drawable is EmbeddedControlRun controlRun + && controlRun.Control is Control control) + { + control.Arrange( + new Rect(new Point(currentX, currentY), + new Size(control.DesiredSize.Width, textLine.Height))); + } + + currentX += drawable.Size.Width; } - - currentX += drawable.Size.Width; } - } - currentY += textLine.Height; + currentY += textLine.Height; + } } + + return finalSize; } protected override AutomationPeer OnCreateAutomationPeer() diff --git a/src/Avalonia.Controls/TopLevel.cs b/src/Avalonia.Controls/TopLevel.cs index 40202255ab..07b1e9b51f 100644 --- a/src/Avalonia.Controls/TopLevel.cs +++ b/src/Avalonia.Controls/TopLevel.cs @@ -1,5 +1,6 @@ using System; using System.ComponentModel; +using System.Diagnostics; using Avalonia.Reactive; using Avalonia.Controls.Metadata; using Avalonia.Controls.Platform; @@ -87,7 +88,15 @@ namespace Avalonia.Controls /// public static readonly StyledProperty RequestedThemeVariantProperty = ThemeVariantScope.RequestedThemeVariantProperty.AddOwner(); - + + /// + /// Defines the SystemBarColor attached property. + /// + public static readonly AttachedProperty SystemBarColorProperty = + AvaloniaProperty.RegisterAttached( + "SystemBarColor", + inherits: true); + /// /// Defines the event. /// @@ -124,6 +133,22 @@ namespace Avalonia.Controls { KeyboardNavigation.TabNavigationProperty.OverrideDefaultValue(KeyboardNavigationMode.Cycle); AffectsMeasure(ClientSizeProperty); + + SystemBarColorProperty.Changed.AddClassHandler((view, e) => + { + if (e.NewValue is SolidColorBrush colorBrush) + { + if (view.Parent is TopLevel tl && tl.InsetsManager is { } insetsManager) + { + insetsManager.SystemBarColor = colorBrush.Color; + } + + if (view is TopLevel topLevel && topLevel.InsetsManager is { } insets) + { + insets.SystemBarColor = colorBrush.Color; + } + } + }); } /// @@ -379,6 +404,26 @@ namespace Avalonia.Controls set { SetValue(AccessText.ShowAccessKeyProperty, value); } } + /// + /// Helper for setting the color of the platform's system bars + /// + /// The main view attached to the toplevel, or the toplevel + /// The color to set + public static void SetSystemBarColor(Control control, SolidColorBrush? color) + { + control.SetValue(SystemBarColorProperty, color); + } + + /// + /// Helper for getting the color of the platform's system bars + /// + /// The main view attached to the toplevel, or the toplevel + /// The current color of the platform's system bars + public static SolidColorBrush? GetSystemBarColor(Control control) + { + return control.GetValue(SystemBarColorProperty); + } + /// double ILayoutRoot.LayoutScaling => PlatformImpl?.RenderScaling ?? 1; @@ -483,6 +528,14 @@ namespace Avalonia.Controls /// protected virtual void HandleClosed() { + Renderer.SceneInvalidated -= SceneInvalidated; + // We need to wait for the renderer to complete any in-flight operations + Renderer.Dispose(); + + Debug.Assert(PlatformImpl != null); + // The PlatformImpl is completely invalid at this point + PlatformImpl = null; + if (_globalStyles is object) { _globalStyles.GlobalStylesAdded -= ((IStyleHost)this).StylesAdded; @@ -492,10 +545,7 @@ namespace Avalonia.Controls { _applicationThemeHost.ActualThemeVariantChanged -= GlobalActualThemeVariantChanged; } - - Renderer.SceneInvalidated -= SceneInvalidated; - Renderer.Dispose(); - + _layoutDiagnosticBridge?.Dispose(); _layoutDiagnosticBridge = null; @@ -503,8 +553,6 @@ namespace Avalonia.Controls _pointerOverPreProcessorSubscription?.Dispose(); _backGestureSubscription?.Dispose(); - PlatformImpl = null; - var logicalArgs = new LogicalTreeAttachmentEventArgs(this, this, null); ((ILogical)this).NotifyDetachedFromLogicalTree(logicalArgs); diff --git a/src/Avalonia.Controls/TreeView.cs b/src/Avalonia.Controls/TreeView.cs index 1a9042a25b..e3a9a05951 100644 --- a/src/Avalonia.Controls/TreeView.cs +++ b/src/Avalonia.Controls/TreeView.cs @@ -10,6 +10,7 @@ using Avalonia.Controls.Generators; using Avalonia.Controls.Primitives; using Avalonia.Input; using Avalonia.Input.Platform; +using Avalonia.Interactivity; using Avalonia.Layout; using Avalonia.Threading; using Avalonia.VisualTree; @@ -60,7 +61,8 @@ namespace Avalonia.Controls /// static TreeView() { - // HACK: Needed or SelectedItem property will not be found in Release build. + SelectingItemsControl.IsSelectedChangedEvent.AddClassHandler((x, e) => + x.ContainerSelectionChanged(e)); } /// @@ -307,12 +309,14 @@ namespace Avalonia.Controls private void SelectSingleItem(object item) { + var oldValue = _selectedItem; _syncingSelectedItems = true; - SelectedItems.Clear(); + SelectedItems.Clear(); + _selectedItem = item; SelectedItems.Add(item); _syncingSelectedItems = false; - SetAndRaise(SelectedItemProperty, ref _selectedItem, item); + RaisePropertyChanged(SelectedItemProperty, oldValue, _selectedItem); } /// @@ -428,9 +432,8 @@ namespace Avalonia.Controls private void MarkItemSelected(object item, bool selected) { - var container = TreeContainerFromItem(item)!; - - MarkContainerSelected(container, selected); + if (TreeContainerFromItem(item) is Control container) + MarkContainerSelected(container, selected); } private void SelectedItemsAdded(IList items) @@ -485,16 +488,24 @@ namespace Avalonia.Controls protected internal override Control CreateContainerForItemOverride() => new TreeViewItem(); protected internal override bool IsItemItsOwnContainerOverride(Control item) => item is TreeViewItem; - protected internal override void PrepareContainerForItemOverride(Control container, object? item, int index) + protected internal override void ContainerForItemPreparedOverride(Control container, object? item, int index) { - base.PrepareContainerForItemOverride(container, item, index); + base.ContainerForItemPreparedOverride(container, item, index); - if (item == SelectedItem) + // Once the container has been full prepared and added to the tree, any bindings from + // styles or item container themes are guaranteed to be applied. + if (container.IsSet(SelectingItemsControl.IsSelectedProperty)) { - MarkContainerSelected(container, true); - if (AutoScrollToSelectedItem) - Dispatcher.UIThread.Post(container.BringIntoView); + // The IsSelected property is set on the container: there is a style or item + // container theme which has bound the IsSelected property. Update our selection + // based on the selection state of the container. + var containerIsSelected = SelectingItemsControl.GetIsSelected(container); + UpdateSelectionFromContainer(container, select: containerIsSelected, toggleModifier: true); } + + // The IsSelected property is not set on the container: update the container + // selection based on the current selection as understood by this control. + MarkContainerSelected(container, SelectedItems.Contains(item)); } /// @@ -661,7 +672,11 @@ namespace Avalonia.Controls var multi = mode.HasAllFlags(SelectionMode.Multiple); var range = multi && rangeModifier && selectedContainer != null; - if (rightButton) + if (!select) + { + SelectedItems.Remove(item); + } + else if (rightButton) { if (!SelectedItems.Contains(item)) { @@ -861,27 +876,44 @@ namespace Avalonia.Controls } /// - /// Sets a container's 'selected' class or . + /// Called when a container raises the + /// . /// - /// The container. - /// Whether the control is selected - private void MarkContainerSelected(Control? container, bool selected) + /// The event. + private void ContainerSelectionChanged(RoutedEventArgs e) { - if (container == null) + if (e.Source is TreeViewItem container && + container.TreeViewOwner == this && + TreeItemFromContainer(container) is object item) { - return; - } + var containerIsSelected = SelectingItemsControl.GetIsSelected(container); + var ourIsSelected = SelectedItems.Contains(item); - if (container is ISelectable selectable) - { - selectable.IsSelected = selected; + if (containerIsSelected != ourIsSelected) + { + if (containerIsSelected) + SelectedItems.Add(item); + else + SelectedItems.Remove(item); + } } - else + + if (e.Source != this) { - ((IPseudoClasses)container.Classes).Set(":selected", selected); + e.Handled = true; } } + /// + /// Sets a container's 'selected' class or . + /// + /// The container. + /// Whether the control is selected + private void MarkContainerSelected(Control container, bool selected) + { + container.SetCurrentValue(SelectingItemsControl.IsSelectedProperty, selected); + } + /// /// Makes a list of objects equal another (though doesn't preserve order). /// diff --git a/src/Avalonia.Controls/TreeViewItem.cs b/src/Avalonia.Controls/TreeViewItem.cs index 5dbfe49533..806d7e320b 100644 --- a/src/Avalonia.Controls/TreeViewItem.cs +++ b/src/Avalonia.Controls/TreeViewItem.cs @@ -31,7 +31,7 @@ namespace Avalonia.Controls /// Defines the property. /// public static readonly StyledProperty IsSelectedProperty = - ListBoxItem.IsSelectedProperty.AddOwner(); + SelectingItemsControl.IsSelectedProperty.AddOwner(); /// /// Defines the property. @@ -105,6 +105,11 @@ namespace Avalonia.Controls EnsureTreeView().PrepareContainerForItemOverride(container, item, index); } + protected internal override void ContainerForItemPreparedOverride(Control container, object? item, int index) + { + EnsureTreeView().ContainerForItemPreparedOverride(container, item, index); + } + /// protected override void OnAttachedToLogicalTree(LogicalTreeAttachmentEventArgs e) { diff --git a/src/Avalonia.Controls/Utils/RealizedStackElements.cs b/src/Avalonia.Controls/Utils/RealizedStackElements.cs new file mode 100644 index 0000000000..8dbfb2c957 --- /dev/null +++ b/src/Avalonia.Controls/Utils/RealizedStackElements.cs @@ -0,0 +1,499 @@ +using System; +using System.Collections.Generic; +using Avalonia.Utilities; + +namespace Avalonia.Controls.Utils +{ + /// + /// Stores the realized element state for a virtualizing panel that arranges its children + /// in a stack layout, such as . + /// + internal class RealizedStackElements + { + private int _firstIndex; + private List? _elements; + private List? _sizes; + private double _startU; + private bool _startUUnstable; + + /// + /// Gets the number of realized elements. + /// + public int Count => _elements?.Count ?? 0; + + /// + /// Gets the index of the first realized element, or -1 if no elements are realized. + /// + public int FirstIndex => _elements?.Count > 0 ? _firstIndex : -1; + + /// + /// Gets the index of the last realized element, or -1 if no elements are realized. + /// + public int LastIndex => _elements?.Count > 0 ? _firstIndex + _elements.Count - 1 : -1; + + /// + /// Gets the elements. + /// + public IReadOnlyList Elements => _elements ??= new List(); + + /// + /// Gets the sizes of the elements on the primary axis. + /// + public IReadOnlyList SizeU => _sizes ??= new List(); + + /// + /// Gets the position of the first element on the primary axis. + /// + public double StartU => _startU; + + /// + /// Adds a newly realized element to the collection. + /// + /// The index of the element. + /// The element. + /// The position of the elemnt on the primary axis. + /// The size of the element on the primary axis. + public void Add(int index, Control element, double u, double sizeU) + { + if (index < 0) + throw new ArgumentOutOfRangeException(nameof(index)); + + _elements ??= new List(); + _sizes ??= new List(); + + if (Count == 0) + { + _elements.Add(element); + _sizes.Add(sizeU); + _startU = u; + _firstIndex = index; + } + else if (index == LastIndex + 1) + { + _elements.Add(element); + _sizes.Add(sizeU); + } + else if (index == FirstIndex - 1) + { + --_firstIndex; + _elements.Insert(0, element); + _sizes.Insert(0, sizeU); + _startU = u; + } + else + { + throw new NotSupportedException("Can only add items to the beginning or end of realized elements."); + } + } + + /// + /// Gets the element at the specified index, if realized. + /// + /// The index in the source collection of the element to get. + /// The element if realized; otherwise null. + public Control? GetElement(int index) + { + var i = index - FirstIndex; + if (i >= 0 && i < _elements?.Count) + return _elements[i]; + return null; + } + + /// + /// Gets or estimates the index and start U position of the anchor element for the + /// specified viewport. + /// + /// The U position of the start of the viewport. + /// The U position of the end of the viewport. + /// The number of items in the list. + /// The current estimated element size. + /// + /// A tuple containing: + /// - The index of the anchor element, or -1 if an anchor could not be determined + /// - The U position of the start of the anchor element, if determined + /// + /// + /// This method tries to find an existing element in the specified viewport from which + /// element realization can start. Failing that it estimates the first element in the + /// viewport. + /// + public (int index, double position) GetOrEstimateAnchorElementForViewport( + double viewportStartU, + double viewportEndU, + int itemCount, + ref double estimatedElementSizeU) + { + // We have no elements, nothing to do here. + if (itemCount <= 0) + return (-1, 0); + + // If we're at 0 then display the first item. + if (MathUtilities.IsZero(viewportStartU)) + return (0, 0); + + if (_sizes is not null && !_startUUnstable) + { + var u = _startU; + + for (var i = 0; i < _sizes.Count; ++i) + { + var size = _sizes[i]; + + if (double.IsNaN(size)) + break; + + var endU = u + size; + + if (endU > viewportStartU && u < viewportEndU) + return (FirstIndex + i, u); + + u = endU; + } + } + + // We don't have any realized elements in the requested viewport, or can't rely on + // StartU being valid. Estimate the index using only the estimated size. First, + // estimate the element size, using defaultElementSizeU if we don't have any realized + // elements. + var estimatedSize = EstimateElementSizeU() switch + { + -1 => estimatedElementSizeU, + double v => v, + }; + + // Store the estimated size for the next layout pass. + estimatedElementSizeU = estimatedSize; + + // Estimate the element at the start of the viewport. + var index = Math.Min((int)(viewportStartU / estimatedSize), itemCount - 1); + return (index, index * estimatedSize); + } + + /// + /// Gets the position of the element with the requested index on the primary axis, if realized. + /// + /// + /// The position of the element, or NaN if the element is not realized. + /// + public double GetElementU(int index) + { + if (index < FirstIndex || _sizes is null) + return double.NaN; + + var endIndex = index - FirstIndex; + + if (endIndex >= _sizes.Count) + return double.NaN; + + var u = StartU; + + for (var i = 0; i < endIndex; ++i) + u += _sizes[i]; + + return u; + } + + public double GetOrEstimateElementU(int index, ref double estimatedElementSizeU) + { + // Return the position of the existing element if realized. + var u = GetElementU(index); + + if (!double.IsNaN(u)) + return u; + + // Estimate the element size, using defaultElementSizeU if we don't have any realized + // elements. + var estimatedSize = EstimateElementSizeU() switch + { + -1 => estimatedElementSizeU, + double v => v, + }; + + // Store the estimated size for the next layout pass. + estimatedElementSizeU = estimatedSize; + + // TODO: Use _startU to work this out. + return index * estimatedSize; + } + + /// + /// Estimates the average U size of all elements in the source collection based on the + /// realized elements. + /// + /// + /// The estimated U size of an element, or -1 if not enough information is present to make + /// an estimate. + /// + public double EstimateElementSizeU() + { + var total = 0.0; + var divisor = 0.0; + + // Average the size of the realized elements. + if (_sizes is not null) + { + foreach (var size in _sizes) + { + if (double.IsNaN(size)) + continue; + total += size; + ++divisor; + } + } + + // We don't have any elements on which to base our estimate. + if (divisor == 0 || total == 0) + return -1; + + return total / divisor; + } + + /// + /// Gets the index of the specified element. + /// + /// The element. + /// The index or -1 if the element is not present in the collection. + public int GetIndex(Control element) + { + return _elements?.IndexOf(element) is int index && index >= 0 ? index + FirstIndex : -1; + } + + /// + /// Updates the elements in response to items being inserted into the source collection. + /// + /// The index in the source collection of the insert. + /// The number of items inserted. + /// A method used to update the element indexes. + public void ItemsInserted(int index, int count, Action updateElementIndex) + { + if (index < 0) + throw new ArgumentOutOfRangeException(nameof(index)); + if (_elements is null || _elements.Count == 0) + return; + + // Get the index within the realized _elements collection. + var first = FirstIndex; + var realizedIndex = index - first; + + if (realizedIndex < Count) + { + // The insertion point affects the realized elements. Update the index of the + // elements after the insertion point. + var elementCount = _elements.Count; + var start = Math.Max(realizedIndex, 0); + var newIndex = realizedIndex + count; + + for (var i = start; i < elementCount; ++i) + { + if (_elements[i] is Control element) + updateElementIndex(element, newIndex - count, newIndex); + ++newIndex; + } + + if (realizedIndex < 0) + { + // The insertion point was before the first element, update the first index. + _firstIndex += count; + } + else + { + // The insertion point was within the realized elements, insert an empty space + // in _elements and _sizes. + _elements!.InsertMany(realizedIndex, null, count); + _sizes!.InsertMany(realizedIndex, double.NaN, count); + } + } + } + + /// + /// Updates the elements in response to items being removed from the source collection. + /// + /// The index in the source collection of the remove. + /// The number of items removed. + /// A method used to update the element indexes. + /// A method used to recycle elements. + public void ItemsRemoved( + int index, + int count, + Action updateElementIndex, + Action recycleElement) + { + if (index < 0) + throw new ArgumentOutOfRangeException(nameof(index)); + if (_elements is null || _elements.Count == 0) + return; + + // Get the removal start and end index within the realized _elements collection. + var first = FirstIndex; + var last = LastIndex; + var startIndex = index - first; + var endIndex = (index + count) - first; + + if (endIndex < 0) + { + // The removed range was before the realized elements. Update the first index and + // the indexes of the realized elements. + _firstIndex -= count; + _startUUnstable = true; + + var newIndex = _firstIndex; + for (var i = 0; i < _elements.Count; ++i) + { + if (_elements[i] is Control element) + updateElementIndex(element, newIndex - count, newIndex); + ++newIndex; + } + } + else if (startIndex < _elements.Count) + { + // Recycle and remove the affected elements. + var start = Math.Max(startIndex, 0); + var end = Math.Min(endIndex, _elements.Count); + + for (var i = start; i < end; ++i) + { + if (_elements[i] is Control element) + recycleElement(element); + } + + _elements.RemoveRange(start, end - start); + _sizes!.RemoveRange(start, end - start); + + // If the remove started before and ended within our realized elements, then our new + // first index will be the index where the remove started. Mark StartU as unstable + // because we can't rely on it now to estimate element heights. + if (startIndex <= 0 && end < last) + { + _firstIndex = first = index; + _startUUnstable = true; + } + + // Update the indexes of the elements after the removed range. + end = _elements.Count; + var newIndex = first + start; + for (var i = start; i < end; ++i) + { + if (_elements[i] is Control element) + updateElementIndex(element, newIndex + count, newIndex); + ++newIndex; + } + } + } + + /// + /// Recycles all elements in response to the source collection being reset. + /// + /// A method used to recycle elements. + public void ItemsReset(Action recycleElement) + { + if (_elements is null || _elements.Count == 0) + return; + + foreach (var e in _elements) + { + if (e is not null) + recycleElement(e); + } + + _startU = _firstIndex = 0; + _elements?.Clear(); + _sizes?.Clear(); + + } + + /// + /// Recycles elements before a specific index. + /// + /// The index in the source collection of new first element. + /// A method used to recycle elements. + public void RecycleElementsBefore(int index, Action recycleElement) + { + if (index <= FirstIndex || _elements is null || _elements.Count == 0) + return; + + if (index > LastIndex) + { + RecycleAllElements(recycleElement); + } + else + { + var endIndex = index - FirstIndex; + + for (var i = 0; i < endIndex; ++i) + { + if (_elements[i] is Control e) + recycleElement(e, i + FirstIndex); + } + + _elements.RemoveRange(0, endIndex); + _sizes!.RemoveRange(0, endIndex); + _firstIndex = index; + } + } + + /// + /// Recycles elements after a specific index. + /// + /// The index in the source collection of new last element. + /// A method used to recycle elements. + public void RecycleElementsAfter(int index, Action recycleElement) + { + if (index >= LastIndex || _elements is null || _elements.Count == 0) + return; + + if (index < FirstIndex) + { + RecycleAllElements(recycleElement); + } + else + { + var startIndex = (index + 1) - FirstIndex; + var count = _elements.Count; + + for (var i = startIndex; i < count; ++i) + { + if (_elements[i] is Control e) + recycleElement(e, i + FirstIndex); + } + + _elements.RemoveRange(startIndex, _elements.Count - startIndex); + _sizes!.RemoveRange(startIndex, _sizes.Count - startIndex); + } + } + + /// + /// Recycles all realized elements. + /// + /// A method used to recycle elements. + public void RecycleAllElements(Action recycleElement) + { + if (_elements is null || _elements.Count == 0) + return; + + var i = FirstIndex; + + foreach (var e in _elements) + { + if (e is not null) + recycleElement(e, i); + ++i; + } + + _startU = _firstIndex = 0; + _elements?.Clear(); + _sizes?.Clear(); + } + + /// + /// Resets the element list and prepares it for reuse. + /// + public void ResetForReuse() + { + _startU = _firstIndex = 0; + _startUUnstable = false; + _elements?.Clear(); + _sizes?.Clear(); + } + } + +} diff --git a/src/Avalonia.Controls/VirtualizingStackPanel.cs b/src/Avalonia.Controls/VirtualizingStackPanel.cs index e86a0de657..77268c7831 100644 --- a/src/Avalonia.Controls/VirtualizingStackPanel.cs +++ b/src/Avalonia.Controls/VirtualizingStackPanel.cs @@ -60,13 +60,14 @@ namespace Avalonia.Controls private readonly Action _recycleElement; private readonly Action _recycleElementOnItemRemoved; private readonly Action _updateElementIndex; - private int _anchorIndex = -1; - private Control? _anchorElement; + private int _scrollToIndex = -1; + private Control? _scrollToElement; private bool _isInLayout; private bool _isWaitingForViewportUpdate; private double _lastEstimatedElementSizeU = 25; - private RealizedElementList? _measureElements; - private RealizedElementList? _realizedElements; + private RealizedStackElements? _measureElements; + private RealizedStackElements? _realizedElements; + private ScrollViewer? _scrollViewer; private Rect _viewport = s_invalidViewport; private Stack? _recyclePool; private Control? _unrealizedFocusedElement; @@ -129,50 +130,55 @@ namespace Avalonia.Controls set { SetValue(AreVerticalSnapPointsRegularProperty, value); } } + /// + /// Gets the index of the first realized element, or -1 if no elements are realized. + /// + public int FirstRealizedIndex => _realizedElements?.FirstIndex ?? -1; + + /// + /// Gets the index of the last realized element, or -1 if no elements are realized. + /// + public int LastRealizedIndex => _realizedElements?.LastIndex ?? -1; + protected override Size MeasureOverride(Size availableSize) { - if (!IsEffectivelyVisible) + var items = Items; + + if (items.Count == 0) return default; + // If we're bringing an item into view, ignore any layout passes until we receive a new + // effective viewport. + if (_isWaitingForViewportUpdate) + return DesiredSize; + _isInLayout = true; try { - var items = Items; var orientation = Orientation; _realizedElements ??= new(); _measureElements ??= new(); - // If we're bringing an item into view, ignore any layout passes until we receive a new - // effective viewport. - if (_isWaitingForViewportUpdate) - { - var sizeV = orientation == Orientation.Horizontal ? DesiredSize.Height : DesiredSize.Width; - return CalculateDesiredSize(orientation, items, sizeV); - } - // We handle horizontal and vertical layouts here so X and Y are abstracted to: // - Horizontal layouts: U = horizontal, V = vertical // - Vertical layouts: U = vertical, V = horizontal var viewport = CalculateMeasureViewport(items); - // Recycle elements outside of the expected range. - _realizedElements.RecycleElementsBefore(viewport.firstIndex, _recycleElement); - _realizedElements.RecycleElementsAfter(viewport.lastIndex, _recycleElement); + // If the viewport is disjunct then we can recycle everything. + if (viewport.viewportIsDisjunct) + _realizedElements.RecycleAllElements(_recycleElement); // Do the measure, creating/recycling elements as necessary to fill the viewport. Don't // write to _realizedElements yet, only _measureElements. - GenerateElements(availableSize, ref viewport); - - // Now we know what definitely fits, recycle anything left over. - _realizedElements.RecycleElementsAfter(_measureElements.LastIndex, _recycleElement); + RealizeElements(items, availableSize, ref viewport); - // And swap the measureElements and realizedElements collection. + // Now swap the measureElements and realizedElements collection. (_measureElements, _realizedElements) = (_realizedElements, _measureElements); _measureElements.ResetForReuse(); - return CalculateDesiredSize(orientation, items, viewport.measuredV); + return CalculateDesiredSize(orientation, items.Count, viewport); } finally { @@ -203,6 +209,7 @@ namespace Avalonia.Controls new Rect(u, 0, sizeU, finalSize.Height) : new Rect(0, u, finalSize.Width, sizeU); e.Arrange(rect); + _scrollViewer?.RegisterAnchorCandidate(e); u += orientation == Orientation.Horizontal ? rect.Width : rect.Height; } } @@ -217,6 +224,18 @@ namespace Avalonia.Controls } } + protected override void OnAttachedToVisualTree(VisualTreeAttachmentEventArgs e) + { + base.OnAttachedToVisualTree(e); + _scrollViewer = this.FindAncestorOfType(); + } + + protected override void OnDetachedFromVisualTree(VisualTreeAttachmentEventArgs e) + { + base.OnDetachedFromVisualTree(e); + _scrollViewer = null; + } + protected override void OnItemsChanged(IReadOnlyList items, NotifyCollectionChangedEventArgs e) { InvalidateMeasure(); @@ -314,7 +333,7 @@ namespace Avalonia.Controls { var items = Items; - if (_isInLayout || index < 0 || index >= items.Count) + if (_isInLayout || index < 0 || index >= items.Count || _realizedElements is null) return null; if (GetRealizedElement(index) is Control element) @@ -326,16 +345,16 @@ namespace Avalonia.Controls { // Create and measure the element to be brought into view. Store it in a field so that // it can be re-used in the layout pass. - _anchorElement = GetOrCreateElement(items, index); - _anchorElement.Measure(Size.Infinity); - _anchorIndex = index; + _scrollToElement = GetOrCreateElement(items, index); + _scrollToElement.Measure(Size.Infinity); + _scrollToIndex = index; // Get the expected position of the elment and put it in place. - var anchorU = GetOrEstimateElementPosition(index); + var anchorU = _realizedElements.GetOrEstimateElementU(index, ref _lastEstimatedElementSizeU); var rect = Orientation == Orientation.Horizontal ? - new Rect(anchorU, 0, _anchorElement.DesiredSize.Width, _anchorElement.DesiredSize.Height) : - new Rect(0, anchorU, _anchorElement.DesiredSize.Width, _anchorElement.DesiredSize.Height); - _anchorElement.Arrange(rect); + new Rect(anchorU, 0, _scrollToElement.DesiredSize.Width, _scrollToElement.DesiredSize.Height) : + new Rect(0, anchorU, _scrollToElement.DesiredSize.Width, _scrollToElement.DesiredSize.Height); + _scrollToElement.Arrange(rect); // If the item being brought into view was added since the last layout pass then // our bounds won't be updated, so any containing scroll viewers will not have an @@ -349,7 +368,7 @@ namespace Avalonia.Controls } // Try to bring the item into view. - _anchorElement.BringIntoView(); + _scrollToElement.BringIntoView(); // If the viewport does not contain the item to scroll to, set _isWaitingForViewportUpdate: // this should cause the following chain of events: @@ -368,9 +387,9 @@ namespace Avalonia.Controls root.LayoutManager.ExecuteLayoutPass(); } - var result = _anchorElement; - _anchorElement = null; - _anchorIndex = -1; + var result = _scrollToElement; + _scrollToElement = null; + _scrollToIndex = -1; return result; } @@ -394,46 +413,40 @@ namespace Avalonia.Controls var viewportStart = Orientation == Orientation.Horizontal ? viewport.X : viewport.Y; var viewportEnd = Orientation == Orientation.Horizontal ? viewport.Right : viewport.Bottom; - var (firstIndex, firstIndexU) = _realizedElements.GetIndexAt(viewportStart); - var (lastIndex, _) = _realizedElements.GetIndexAt(viewportEnd); - var estimatedElementSize = -1.0; + // Get or estimate the anchor element from which to start realization. var itemCount = items?.Count ?? 0; - var maxIndex = Math.Max(itemCount - 1, 0); - - if (firstIndex == -1) - { - estimatedElementSize = EstimateElementSizeU(); - firstIndex = Math.Min((int)(viewportStart / estimatedElementSize), maxIndex); - firstIndexU = firstIndex * estimatedElementSize; - } + var (anchorIndex, anchorU) = _realizedElements.GetOrEstimateAnchorElementForViewport( + viewportStart, + viewportEnd, + itemCount, + ref _lastEstimatedElementSizeU); - if (lastIndex == -1) - { - if (estimatedElementSize == -1) - estimatedElementSize = EstimateElementSizeU(); - lastIndex = Math.Min((int)(viewportEnd / estimatedElementSize), maxIndex); - } + // Check if the anchor element is not within the currently realized elements. + var disjunct = anchorIndex < _realizedElements.FirstIndex || + anchorIndex > _realizedElements.LastIndex; return new MeasureViewport { - firstIndex = firstIndex, - lastIndex = lastIndex, + anchorIndex = anchorIndex, + anchorU = anchorU, viewportUStart = viewportStart, viewportUEnd = viewportEnd, - startU = firstIndexU, + viewportIsDisjunct = disjunct, }; } - private Size CalculateDesiredSize(Orientation orientation, IReadOnlyList items, double sizeV) + private Size CalculateDesiredSize(Orientation orientation, int itemCount, in MeasureViewport viewport) { - var sizeU = EstimateElementSizeU() * items.Count; + var sizeU = 0.0; + var sizeV = viewport.measuredV; - if (double.IsInfinity(sizeU) || double.IsNaN(sizeU)) - throw new InvalidOperationException("Invalid calculated size."); + if (viewport.lastIndex >= 0) + { + var remaining = itemCount - viewport.lastIndex - 1; + sizeU = viewport.realizedEndU + (remaining * _lastEstimatedElementSizeU); + } - return orientation == Orientation.Horizontal ? - new Size(sizeU, sizeV) : - new Size(sizeV, sizeU); + return orientation == Orientation.Horizontal ? new(sizeU, sizeV) : new(sizeV, sizeU); } private double EstimateElementSizeU() @@ -474,19 +487,25 @@ namespace Avalonia.Controls return viewport; } - private void GenerateElements(Size availableSize, ref MeasureViewport viewport) + private void RealizeElements( + IReadOnlyList items, + Size availableSize, + ref MeasureViewport viewport) { Debug.Assert(_measureElements is not null); + Debug.Assert(_realizedElements is not null); + Debug.Assert(items.Count > 0); - var items = Items; + var index = viewport.anchorIndex; var horizontal = Orientation == Orientation.Horizontal; - var index = viewport.firstIndex; - var u = viewport.startU; + var u = viewport.anchorU; - // The layout is likely invalid. Don't create any elements and instead rely on our previous - // element size estimates to calculate a new desired size and trigger a new layout pass. - if (index >= items.Count) - return; + // If the anchor element is at the beginning of, or before, the start of the viewport + // then we can recycle all elements before it. + if (u <= viewport.anchorU) + _realizedElements.RecycleElementsBefore(viewport.anchorIndex, _recycleElement); + + // Start at the anchor element and move forwards, realizing elements. do { var e = GetOrCreateElement(items, index); @@ -501,6 +520,34 @@ namespace Avalonia.Controls u += sizeU; ++index; } while (u < viewport.viewportUEnd && index < items.Count); + + // Store the last index and end U position for the desired size calculation. + viewport.lastIndex = index - 1; + viewport.realizedEndU = u; + + // We can now recycle elements after the last element. + _realizedElements.RecycleElementsAfter(viewport.lastIndex, _recycleElement); + + // Next move backwards from the anchor element, realizing elements. + index = viewport.anchorIndex - 1; + u = viewport.anchorU; + + while (u > viewport.viewportUStart && index >= 0) + { + var e = GetOrCreateElement(items, index); + e.Measure(availableSize); + + var sizeU = horizontal ? e.DesiredSize.Width : e.DesiredSize.Height; + var sizeV = horizontal ? e.DesiredSize.Height : e.DesiredSize.Width; + u -= sizeU; + + _measureElements!.Add(index, e, u, sizeU); + viewport.measuredV = Math.Max(viewport.measuredV, sizeV); + --index; + } + + // We can now recycle elements before the first element. + _realizedElements.RecycleElementsBefore(index + 1, _recycleElement); } private Control GetOrCreateElement(IReadOnlyList items, int index) @@ -515,8 +562,8 @@ namespace Avalonia.Controls private Control? GetRealizedElement(int index) { - if (_anchorIndex == index) - return _anchorElement; + if (_scrollToIndex == index) + return _scrollToElement; return _realizedElements?.GetElement(index); } @@ -554,13 +601,15 @@ namespace Avalonia.Controls var generator = ItemContainerGenerator!; var item = items[index]; - if (_unrealizedFocusedIndex == index) + if (_unrealizedFocusedIndex == index && _unrealizedFocusedElement is not null) { var element = _unrealizedFocusedElement; + _unrealizedFocusedElement.LostFocus -= OnUnrealizedFocusedElementLostFocus; _unrealizedFocusedElement = null; _unrealizedFocusedIndex = -1; return element; } + if (_recyclePool?.Count > 0) { var recycled = _recyclePool.Pop(); @@ -588,16 +637,12 @@ namespace Avalonia.Controls return container; } - private double GetOrEstimateElementPosition(int index) - { - var estimatedElementSize = EstimateElementSizeU(); - return index * estimatedElementSize; - } - private void RecycleElement(Control element, int index) { Debug.Assert(ItemContainerGenerator is not null); + _scrollViewer?.UnregisterAnchorCandidate(element); + if (element.IsSet(ItemIsOwnContainerProperty)) { element.IsVisible = false; @@ -606,6 +651,7 @@ namespace Avalonia.Controls { _unrealizedFocusedElement = element; _unrealizedFocusedIndex = index; + _unrealizedFocusedElement.LostFocus += OnUnrealizedFocusedElementLostFocus; } else { @@ -646,7 +692,7 @@ namespace Avalonia.Controls var oldViewportStart = vertical ? _viewport.Top : _viewport.Left; var oldViewportEnd = vertical ? _viewport.Bottom : _viewport.Right; - _viewport = e.EffectiveViewport; + _viewport = e.EffectiveViewport.Intersect(new(Bounds.Size)); _isWaitingForViewportUpdate = false; var newViewportStart = vertical ? _viewport.Top : _viewport.Left; @@ -692,6 +738,17 @@ namespace Avalonia.Controls Invalidate(c); } + private void OnUnrealizedFocusedElementLostFocus(object? sender, RoutedEventArgs e) + { + if (_unrealizedFocusedElement is null || sender != _unrealizedFocusedElement) + return; + + _unrealizedFocusedElement.LostFocus -= OnUnrealizedFocusedElementLostFocus; + RecycleElement(_unrealizedFocusedElement, _unrealizedFocusedIndex); + _unrealizedFocusedElement = null; + _unrealizedFocusedIndex = -1; + } + /// public IReadOnlyList GetIrregularSnapPoints(Orientation orientation, SnapPointsAlignment snapPointsAlignment) { @@ -853,455 +910,16 @@ namespace Avalonia.Controls return snapPoint; } - /// - /// Stores the realized element state for a . - /// - internal class RealizedElementList - { - private int _firstIndex; - private List? _elements; - private List? _sizes; - private double _startU; - private bool _startUUnstable; - - /// - /// Gets the number of realized elements. - /// - public int Count => _elements?.Count ?? 0; - - /// - /// Gets the index of the first realized element, or -1 if no elements are realized. - /// - public int FirstIndex => _elements?.Count > 0 ? _firstIndex : -1; - - /// - /// Gets the index of the last realized element, or -1 if no elements are realized. - /// - public int LastIndex => _elements?.Count > 0 ? _firstIndex + _elements.Count - 1 : -1; - - /// - /// Gets the elements. - /// - public IReadOnlyList Elements => _elements ??= new List(); - - /// - /// Gets the sizes of the elements on the primary axis. - /// - public IReadOnlyList SizeU => _sizes ??= new List(); - - /// - /// Gets the position of the first element on the primary axis. - /// - public double StartU => _startU; - - /// - /// Adds a newly realized element to the collection. - /// - /// The index of the element. - /// The element. - /// The position of the elemnt on the primary axis. - /// The size of the element on the primary axis. - public void Add(int index, Control element, double u, double sizeU) - { - if (index < 0) - throw new ArgumentOutOfRangeException(nameof(index)); - - _elements ??= new List(); - _sizes ??= new List(); - - if (Count == 0) - { - _elements.Add(element); - _sizes.Add(sizeU); - _startU = u; - _firstIndex = index; - } - else if (index == LastIndex + 1) - { - _elements.Add(element); - _sizes.Add(sizeU); - } - else if (index == FirstIndex - 1) - { - --_firstIndex; - _elements.Insert(0, element); - _sizes.Insert(0, sizeU); - _startU = u; - } - else - { - throw new NotSupportedException("Can only add items to the beginning or end of realized elements."); - } - } - - /// - /// Gets the element at the specified index, if realized. - /// - /// The index in the source collection of the element to get. - /// The element if realized; otherwise null. - public Control? GetElement(int index) - { - var i = index - FirstIndex; - if (i >= 0 && i < _elements?.Count) - return _elements[i]; - return null; - } - - /// - /// Gets the index and start U position of the element at the specified U position. - /// - /// The U position. - /// - /// A tuple containing: - /// - The index of the item at the specified U position, or -1 if the item could not be - /// determined - /// - The U position of the start of the item, if determined - /// - public (int index, double position) GetIndexAt(double u) - { - if (_elements is null || _sizes is null || _startU > u || _startUUnstable) - return (-1, 0); - - var index = 0; - var position = _startU; - - while (index < _elements.Count) - { - var size = _sizes[index]; - if (double.IsNaN(size)) - break; - if (u >= position && u < position + size) - return (index + FirstIndex, position); - position += size; - ++index; - } - - return (-1, 0); - } - - /// - /// Gets the element at the specified position on the primary axis, if realized. - /// - /// The position. - /// - /// A tuple containing the index of the element (or -1 if not found) and the position of the element on the - /// primary axis. - /// - public (int index, double position) GetElementAt(double position) - { - if (_sizes is null || position < StartU) - return (-1, 0); - - var u = StartU; - var i = FirstIndex; - - foreach (var size in _sizes) - { - var endU = u + size; - if (position < endU) - return (i, u); - u += size; - ++i; - } - - return (-1, 0); - } - - /// - /// Estimates the average U size of all elements in the source collection based on the - /// realized elements. - /// - /// - /// The estimated U size of an element, or -1 if not enough information is present to make - /// an estimate. - /// - public double EstimateElementSizeU() - { - var total = 0.0; - var divisor = 0.0; - - // Start by averaging the size of the elements before the first realized element. - if (FirstIndex >= 0 && !_startUUnstable) - { - total += _startU; - divisor += FirstIndex; - } - - // Average the size of the realized elements. - if (_sizes is not null) - { - foreach (var size in _sizes) - { - if (double.IsNaN(size)) - continue; - total += size; - ++divisor; - } - } - - // We don't have any elements on which to base our estimate. - if (divisor == 0 || total == 0) - return -1; - - return total / divisor; - } - - /// - /// Gets the index of the specified element. - /// - /// The element. - /// The index or -1 if the element is not present in the collection. - public int GetIndex(Control element) - { - return _elements?.IndexOf(element) is int index && index >= 0 ? index + FirstIndex : -1; - } - - /// - /// Updates the elements in response to items being inserted into the source collection. - /// - /// The index in the source collection of the insert. - /// The number of items inserted. - /// A method used to update the element indexes. - public void ItemsInserted(int index, int count, Action updateElementIndex) - { - if (index < 0) - throw new ArgumentOutOfRangeException(nameof(index)); - if (_elements is null || _elements.Count == 0) - return; - - // Get the index within the realized _elements collection. - var first = FirstIndex; - var realizedIndex = index - first; - - if (realizedIndex < Count) - { - // The insertion point affects the realized elements. Update the index of the - // elements after the insertion point. - var elementCount = _elements.Count; - var start = Math.Max(realizedIndex, 0); - var newIndex = realizedIndex + count; - - for (var i = start; i < elementCount; ++i) - { - if (_elements[i] is Control element) - updateElementIndex(element, newIndex - count, newIndex); - ++newIndex; - } - - if (realizedIndex < 0) - { - // The insertion point was before the first element, update the first index. - _firstIndex += count; - } - else - { - // The insertion point was within the realized elements, insert an empty space - // in _elements and _sizes. - _elements!.InsertMany(realizedIndex, null, count); - _sizes!.InsertMany(realizedIndex, double.NaN, count); - } - } - } - - /// - /// Updates the elements in response to items being removed from the source collection. - /// - /// The index in the source collection of the remove. - /// The number of items removed. - /// A method used to update the element indexes. - /// A method used to recycle elements. - public void ItemsRemoved( - int index, - int count, - Action updateElementIndex, - Action recycleElement) - { - if (index < 0) - throw new ArgumentOutOfRangeException(nameof(index)); - if (_elements is null || _elements.Count == 0) - return; - - // Get the removal start and end index within the realized _elements collection. - var first = FirstIndex; - var last = LastIndex; - var startIndex = index - first; - var endIndex = (index + count) - first; - - if (endIndex < 0) - { - // The removed range was before the realized elements. Update the first index and - // the indexes of the realized elements. - _firstIndex -= count; - _startUUnstable = true; - - var newIndex = _firstIndex; - for (var i = 0; i < _elements.Count; ++i) - { - if (_elements[i] is Control element) - updateElementIndex(element, newIndex - count, newIndex); - ++newIndex; - } - } - else if (startIndex < _elements.Count) - { - // Recycle and remove the affected elements. - var start = Math.Max(startIndex, 0); - var end = Math.Min(endIndex, _elements.Count); - - for (var i = start; i < end; ++i) - { - if (_elements[i] is Control element) - recycleElement(element); - } - - _elements.RemoveRange(start, end - start); - _sizes!.RemoveRange(start, end - start); - - // If the remove started before and ended within our realized elements, then our new - // first index will be the index where the remove started. Mark StartU as unstable - // because we can't rely on it now to estimate element heights. - if (startIndex <= 0 && end < last) - { - _firstIndex = first = index; - _startUUnstable = true; - } - - // Update the indexes of the elements after the removed range. - end = _elements.Count; - var newIndex = first + start; - for (var i = start; i < end; ++i) - { - if (_elements[i] is Control element) - updateElementIndex(element, newIndex + count, newIndex); - ++newIndex; - } - } - } - - /// - /// Recycles all elements in response to the source collection being reset. - /// - /// A method used to recycle elements. - public void ItemsReset(Action recycleElement) - { - if (_elements is null || _elements.Count == 0) - return; - - foreach (var e in _elements) - { - if (e is not null) - recycleElement(e); - } - - _startU = _firstIndex = 0; - _elements?.Clear(); - _sizes?.Clear(); - - } - - /// - /// Recycles elements before a specific index. - /// - /// The index in the source collection of new first element. - /// A method used to recycle elements. - public void RecycleElementsBefore(int index, Action recycleElement) - { - if (index <= FirstIndex || _elements is null || _elements.Count == 0) - return; - - if (index > LastIndex) - { - RecycleAllElements(recycleElement); - } - else - { - var endIndex = index - FirstIndex; - - for (var i = 0; i < endIndex; ++i) - { - if (_elements[i] is Control e) - recycleElement(e, i + FirstIndex); - } - - _elements.RemoveRange(0, endIndex); - _sizes!.RemoveRange(0, endIndex); - _firstIndex = index; - } - } - - /// - /// Recycles elements after a specific index. - /// - /// The index in the source collection of new last element. - /// A method used to recycle elements. - public void RecycleElementsAfter(int index, Action recycleElement) - { - if (index >= LastIndex || _elements is null || _elements.Count == 0) - return; - - if (index < FirstIndex) - { - RecycleAllElements(recycleElement); - } - else - { - var startIndex = (index + 1) - FirstIndex; - var count = _elements.Count; - - for (var i = startIndex; i < count; ++i) - { - if (_elements[i] is Control e) - recycleElement(e, i + FirstIndex); - } - - _elements.RemoveRange(startIndex, _elements.Count - startIndex); - _sizes!.RemoveRange(startIndex, _sizes.Count - startIndex); - } - } - - /// - /// Recycles all realized elements. - /// - /// A method used to recycle elements. - public void RecycleAllElements(Action recycleElement) - { - if (_elements is null || _elements.Count == 0) - return; - - var i = FirstIndex; - - foreach (var e in _elements) - { - if (e is not null) - recycleElement(e, i); - ++i; - } - - _startU = _firstIndex = 0; - _elements?.Clear(); - _sizes?.Clear(); - } - - /// - /// Resets the element list and prepares it for reuse. - /// - public void ResetForReuse() - { - _startU = _firstIndex = 0; - _startUUnstable = false; - _elements?.Clear(); - _sizes?.Clear(); - } - } - private struct MeasureViewport { - public int firstIndex; - public int lastIndex; + public int anchorIndex; + public double anchorU; public double viewportUStart; public double viewportUEnd; public double measuredV; - public double startU; + public double realizedEndU; + public int lastIndex; + public bool viewportIsDisjunct; } } } diff --git a/src/Avalonia.Diagnostics/Diagnostics/Controls/BrushEditor.cs b/src/Avalonia.Diagnostics/Diagnostics/Controls/BrushEditor.cs index b7579ed31b..ff05614667 100644 --- a/src/Avalonia.Diagnostics/Diagnostics/Controls/BrushEditor.cs +++ b/src/Avalonia.Diagnostics/Diagnostics/Controls/BrushEditor.cs @@ -34,7 +34,11 @@ namespace Avalonia.Diagnostics.Controls { case ISolidColorBrush scb: { - var colorView = new ColorView { Color = scb.Color }; + var colorView = new ColorView + { + HexInputAlphaPosition = AlphaComponentPosition.Leading, // Always match XAML + Color = scb.Color, + }; colorView.ColorChanged += (_, e) => Brush = new ImmutableSolidColorBrush(e.NewColor); diff --git a/src/Avalonia.Diagnostics/Diagnostics/Views/PropertyValueEditorView.cs b/src/Avalonia.Diagnostics/Diagnostics/Views/PropertyValueEditorView.cs index 6b52989f0b..7b58671996 100644 --- a/src/Avalonia.Diagnostics/Diagnostics/Views/PropertyValueEditorView.cs +++ b/src/Avalonia.Diagnostics/Diagnostics/Views/PropertyValueEditorView.cs @@ -64,11 +64,12 @@ namespace Avalonia.Diagnostics.Views where TControl : Control, new() { var control = new TControl(); + var bindingMode = Property.IsReadonly ? BindingMode.OneWay : BindingMode.TwoWay; init?.Invoke(control); control.Bind(valueProperty, - new Binding(nameof(Property.Value), BindingMode.TwoWay) + new Binding(nameof(Property.Value), bindingMode) { Source = Property, Converter = converter ?? new ValueConverter(), @@ -129,7 +130,10 @@ namespace Avalonia.Diagnostics.Views IsEnabled = !Property.IsReadonly }; - var cv = new ColorView(); + var cv = new ColorView + { + HexInputAlphaPosition = AlphaComponentPosition.Leading, // Always match XAML + }; cv.Bind( ColorView.ColorProperty, diff --git a/src/Avalonia.Fonts.Inter/AppBuilderExtension.cs b/src/Avalonia.Fonts.Inter/AppBuilderExtension.cs index 842629c923..e63032aa56 100644 --- a/src/Avalonia.Fonts.Inter/AppBuilderExtension.cs +++ b/src/Avalonia.Fonts.Inter/AppBuilderExtension.cs @@ -1,4 +1,6 @@ -namespace Avalonia.Fonts.Inter +using Avalonia.Fonts.Inter; + +namespace Avalonia { public static class AppBuilderExtension { diff --git a/src/Avalonia.FreeDesktop/Avalonia.FreeDesktop.csproj b/src/Avalonia.FreeDesktop/Avalonia.FreeDesktop.csproj index da9add1fa4..d8162c0486 100644 --- a/src/Avalonia.FreeDesktop/Avalonia.FreeDesktop.csproj +++ b/src/Avalonia.FreeDesktop/Avalonia.FreeDesktop.csproj @@ -5,20 +5,20 @@ enable - + - + - + - - + + diff --git a/src/Avalonia.FreeDesktop/DBusMenuExporter.cs b/src/Avalonia.FreeDesktop/DBusMenuExporter.cs index 3073ea580c..77f9d31273 100644 --- a/src/Avalonia.FreeDesktop/DBusMenuExporter.cs +++ b/src/Avalonia.FreeDesktop/DBusMenuExporter.cs @@ -38,7 +38,7 @@ namespace Avalonia.FreeDesktop private bool _resetQueued; private int _nextId = 1; - public DBusMenuExporterImpl(Connection connection, IntPtr xid) + public DBusMenuExporterImpl(Connection connection, IntPtr xid) : this() { Connection = connection; _xid = (uint)xid.ToInt32(); @@ -47,7 +47,7 @@ namespace Avalonia.FreeDesktop _ = InitializeAsync(); } - public DBusMenuExporterImpl(Connection connection, string path) + public DBusMenuExporterImpl(Connection connection, string path) : this() { Connection = connection; _appMenu = false; @@ -56,6 +56,13 @@ namespace Avalonia.FreeDesktop _ = InitializeAsync(); } + private DBusMenuExporterImpl() + { + BackingProperties.Status = string.Empty; + BackingProperties.TextDirection = string.Empty; + BackingProperties.IconThemePath = Array.Empty(); + } + protected override Connection Connection { get; } public override string Path { get; } @@ -202,15 +209,9 @@ namespace Avalonia.FreeDesktop return id; } - private void OnMenuItemsChanged(object? sender, NotifyCollectionChangedEventArgs e) - { - QueueReset(); - } + private void OnMenuItemsChanged(object? sender, NotifyCollectionChangedEventArgs e) => QueueReset(); - private void OnItemPropertyChanged(object? sender, AvaloniaPropertyChangedEventArgs e) - { - QueueReset(); - } + private void OnItemPropertyChanged(object? sender, AvaloniaPropertyChangedEventArgs e) => QueueReset(); private static readonly string[] s_allProperties = { "type", "label", "enabled", "visible", "shortcut", "toggle-type", "children-display", "toggle-state", "icon-data" diff --git a/src/Avalonia.FreeDesktop/DBusTrayIconImpl.cs b/src/Avalonia.FreeDesktop/DBusTrayIconImpl.cs index fed8b87bc9..43ae48341c 100644 --- a/src/Avalonia.FreeDesktop/DBusTrayIconImpl.cs +++ b/src/Avalonia.FreeDesktop/DBusTrayIconImpl.cs @@ -220,6 +220,16 @@ namespace Avalonia.FreeDesktop { Connection = connection; BackingProperties.Menu = dbusMenuPath; + BackingProperties.Category = string.Empty; + BackingProperties.Status = string.Empty; + BackingProperties.Id = string.Empty; + BackingProperties.Title = string.Empty; + BackingProperties.IconPixmap = Array.Empty<(int, int, byte[])>(); + BackingProperties.AttentionIconName = string.Empty; + BackingProperties.AttentionIconPixmap = Array.Empty<(int, int, byte[])>(); + BackingProperties.AttentionMovieName = string.Empty; + BackingProperties.OverlayIconName = string.Empty; + BackingProperties.OverlayIconPixmap = Array.Empty<(int, int, byte[])>(); BackingProperties.ToolTip = (string.Empty, Array.Empty<(int, int, byte[])>(), string.Empty, string.Empty); InvalidateAll(); } @@ -234,7 +244,7 @@ namespace Avalonia.FreeDesktop protected override ValueTask OnActivateAsync(int x, int y) { - Dispatcher.UIThread.Post(() => ActivationDelegate?.Invoke()); + ActivationDelegate?.Invoke(); return new ValueTask(); } @@ -267,7 +277,6 @@ namespace Avalonia.FreeDesktop BackingProperties.Category = "ApplicationStatus"; BackingProperties.Status = text; BackingProperties.Title = text; - BackingProperties.ToolTip = (string.Empty, Array.Empty<(int, int, byte[])>(), text, string.Empty); InvalidateAll(); } } diff --git a/src/Avalonia.Headless/AvaloniaHeadlessPlatform.cs b/src/Avalonia.Headless/AvaloniaHeadlessPlatform.cs index 319b0da7bf..90749caf6f 100644 --- a/src/Avalonia.Headless/AvaloniaHeadlessPlatform.cs +++ b/src/Avalonia.Headless/AvaloniaHeadlessPlatform.cs @@ -43,6 +43,8 @@ namespace Avalonia.Headless _framesPerSecond = framesPerSecond; } + public override bool RunsInBackground => false; + public void ForceTick() => _forceTick?.Invoke(); } diff --git a/src/Avalonia.Headless/HeadlessPlatformRenderInterface.cs b/src/Avalonia.Headless/HeadlessPlatformRenderInterface.cs index f8100d3832..431989134a 100644 --- a/src/Avalonia.Headless/HeadlessPlatformRenderInterface.cs +++ b/src/Avalonia.Headless/HeadlessPlatformRenderInterface.cs @@ -47,8 +47,8 @@ namespace Avalonia.Headless } public IStreamGeometryImpl CreateStreamGeometry() => new HeadlessStreamingGeometryStub(); - public IGeometryImpl CreateGeometryGroup(FillRule fillRule, IReadOnlyList children) => throw new NotImplementedException(); - public IGeometryImpl CreateCombinedGeometry(GeometryCombineMode combineMode, Geometry g1, Geometry g2) => throw new NotImplementedException(); + public IGeometryImpl CreateGeometryGroup(FillRule fillRule, IReadOnlyList children) => throw new NotImplementedException(); + public IGeometryImpl CreateCombinedGeometry(GeometryCombineMode combineMode, IGeometryImpl g1, IGeometryImpl g2) => throw new NotImplementedException(); public IRenderTarget CreateRenderTarget(IEnumerable surfaces) => new HeadlessRenderTarget(); public bool IsLost => false; diff --git a/src/Avalonia.Headless/HeadlessPlatformStubs.cs b/src/Avalonia.Headless/HeadlessPlatformStubs.cs index ee4cd5af98..aa400ab3e6 100644 --- a/src/Avalonia.Headless/HeadlessPlatformStubs.cs +++ b/src/Avalonia.Headless/HeadlessPlatformStubs.cs @@ -179,8 +179,7 @@ namespace Avalonia.Headless return true; } - public bool TryMatchCharacter(int codepoint, FontStyle fontStyle, FontWeight fontWeight, FontStretch fontStretch, - FontFamily fontFamily, CultureInfo culture, out Typeface typeface) + public bool TryMatchCharacter(int codepoint, FontStyle fontStyle, FontWeight fontWeight, FontStretch fontStretch, CultureInfo culture, out Typeface typeface) { typeface = new Typeface("Arial", fontStyle, fontWeight, fontStretch); return true; diff --git a/src/Avalonia.Native/AvaloniaNativeTextInputMethod.cs b/src/Avalonia.Native/AvaloniaNativeTextInputMethod.cs index 796a0ced37..4c93c06ef3 100644 --- a/src/Avalonia.Native/AvaloniaNativeTextInputMethod.cs +++ b/src/Avalonia.Native/AvaloniaNativeTextInputMethod.cs @@ -2,12 +2,14 @@ using Avalonia.Input.TextInput; using Avalonia.Native.Interop; +#nullable enable + namespace Avalonia.Native { internal class AvaloniaNativeTextInputMethod : ITextInputMethodImpl, IDisposable { - private ITextInputMethodClient _client; - private IAvnTextInputMethodClient _nativeClient; + private ITextInputMethodClient? _client; + private IAvnTextInputMethodClient? _nativeClient; private readonly IAvnTextInputMethod _inputMethod; public AvaloniaNativeTextInputMethod(IAvnWindowBase nativeWindow) @@ -26,7 +28,7 @@ namespace Avalonia.Native _inputMethod.Reset(); } - public void SetClient(ITextInputMethodClient client) + public void SetClient(ITextInputMethodClient? client) { if (_client is { SupportsSurroundingText: true }) { @@ -39,9 +41,9 @@ namespace Avalonia.Native _nativeClient = null; _client = client; - if (client != null) + if (_client != null) { - _nativeClient = new AvnTextInputMethodClient(client); + _nativeClient = new AvnTextInputMethodClient(_client); OnSurroundingTextChanged(this, EventArgs.Empty); OnCursorRectangleChanged(this, EventArgs.Empty); @@ -53,16 +55,28 @@ namespace Avalonia.Native _inputMethod.SetClient(_nativeClient); } - private void OnCursorRectangleChanged(object sender, EventArgs e) + private void OnCursorRectangleChanged(object? sender, EventArgs e) { if (_client == null) { return; } - var visualRoot = _client.TextViewVisual.VisualRoot; + var textViewVisual = _client.TextViewVisual; + + if(textViewVisual is null ) + { + return; + } + + var visualRoot = textViewVisual.VisualRoot; + + if(visualRoot is null) + { + return; + } - var transform = _client.TextViewVisual.TransformToVisual((Visual)visualRoot); + var transform = textViewVisual.TransformToVisual((Visual)visualRoot); if (transform == null) { @@ -74,7 +88,7 @@ namespace Avalonia.Native _inputMethod.SetCursorRect(rect.ToAvnRect()); } - private void OnSurroundingTextChanged(object sender, EventArgs e) + private void OnSurroundingTextChanged(object? sender, EventArgs e) { if (_client == null) { @@ -84,7 +98,7 @@ namespace Avalonia.Native var surroundingText = _client.SurroundingText; _inputMethod.SetSurroundingText( - surroundingText.Text, + surroundingText.Text ?? "", surroundingText.AnchorOffset, surroundingText.CursorOffset ); diff --git a/src/Avalonia.Native/WindowImpl.cs b/src/Avalonia.Native/WindowImpl.cs index 5d0e6a2d18..817fe3d080 100644 --- a/src/Avalonia.Native/WindowImpl.cs +++ b/src/Avalonia.Native/WindowImpl.cs @@ -21,6 +21,7 @@ namespace Avalonia.Native private DoubleClickHelper _doubleClickHelper; private readonly ITopLevelNativeMenuExporter _nativeMenuExporter; private readonly AvaloniaNativeTextInputMethod _inputMethod; + private bool _canResize = true; internal WindowImpl(IAvaloniaNativeFactory factory, AvaloniaNativePlatformOptions opts, AvaloniaNativeGlPlatformGraphics glFeature) : base(factory, opts, glFeature) @@ -75,6 +76,7 @@ namespace Avalonia.Native public void CanResize(bool value) { + _canResize = value; _native.SetCanResize(value.AsComBool()); } @@ -137,14 +139,10 @@ namespace Avalonia.Native { if (_doubleClickHelper.IsDoubleClick(e.Timestamp, e.Position)) { - // TOGGLE WINDOW STATE. - if (WindowState == WindowState.Maximized || WindowState == WindowState.FullScreen) + if (_canResize) { - WindowState = WindowState.Normal; - } - else - { - WindowState = WindowState.Maximized; + WindowState = WindowState is WindowState.Maximized or WindowState.FullScreen ? + WindowState.Normal : WindowState.Maximized; } } else diff --git a/src/Avalonia.Themes.Fluent/Controls/CaptionButtons.xaml b/src/Avalonia.Themes.Fluent/Controls/CaptionButtons.xaml index 71ae012289..666717acde 100644 --- a/src/Avalonia.Themes.Fluent/Controls/CaptionButtons.xaml +++ b/src/Avalonia.Themes.Fluent/Controls/CaptionButtons.xaml @@ -48,7 +48,8 @@