committed by
GitHub
75 changed files with 4222 additions and 2844 deletions
@ -0,0 +1,14 @@ |
|||
<Application xmlns="https://github.com/avaloniaui" |
|||
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" |
|||
x:Class="VirtualizationDemo.App"> |
|||
<Application.Styles> |
|||
<FluentTheme/> |
|||
</Application.Styles> |
|||
<Application.Resources> |
|||
<ResourceDictionary> |
|||
<ResourceDictionary.MergedDictionaries> |
|||
<ResourceInclude Source="avares://ControlSamples/HamburgerMenu/HamburgerMenu.xaml" /> |
|||
</ResourceDictionary.MergedDictionaries> |
|||
</ResourceDictionary> |
|||
</Application.Resources> |
|||
</Application> |
|||
@ -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(); |
|||
} |
|||
} |
|||
@ -1,7 +0,0 @@ |
|||
<Application xmlns="https://github.com/avaloniaui" |
|||
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" |
|||
x:Class="VirtualizationDemo.App"> |
|||
<Application.Styles> |
|||
<SimpleTheme /> |
|||
</Application.Styles> |
|||
</Application> |
|||
@ -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(); |
|||
} |
|||
} |
|||
} |
|||
@ -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" |
|||
} |
|||
] |
|||
} |
|||
|
|||
@ -0,0 +1,20 @@ |
|||
<Window xmlns="https://github.com/avaloniaui" |
|||
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" |
|||
xmlns:controls="using:ControlSamples" |
|||
xmlns:vm="using:VirtualizationDemo.ViewModels" |
|||
xmlns:views="using:VirtualizationDemo.Views" |
|||
x:Class="VirtualizationDemo.MainWindow" |
|||
Title="AvaloniaUI Virtualization Demo" |
|||
x:DataType="vm:MainWindowViewModel"> |
|||
<controls:HamburgerMenu> |
|||
<TabItem Header="Playground" ScrollViewer.VerticalScrollBarVisibility="Disabled"> |
|||
<views:PlaygroundPageView DataContext="{Binding Playground}"/> |
|||
</TabItem> |
|||
<TabItem Header="Chat" ScrollViewer.VerticalScrollBarVisibility="Disabled"> |
|||
<views:ChatPageView DataContext="{Binding Chat}"/> |
|||
</TabItem> |
|||
<TabItem Header="Expanders" ScrollViewer.VerticalScrollBarVisibility="Disabled"> |
|||
<views:ExpanderPageView DataContext="{Binding Expanders}"/> |
|||
</TabItem> |
|||
</controls:HamburgerMenu> |
|||
</Window> |
|||
@ -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(); |
|||
} |
|||
} |
|||
@ -1,64 +0,0 @@ |
|||
<Window xmlns="https://github.com/avaloniaui" |
|||
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" |
|||
xmlns:viewModels="using:VirtualizationDemo.ViewModels" |
|||
x:Class="VirtualizationDemo.MainWindow" |
|||
Title="AvaloniaUI Virtualization Test" |
|||
Width="800" |
|||
Height="600" |
|||
x:DataType="viewModels:MainWindowViewModel"> |
|||
<DockPanel LastChildFill="True" Margin="16"> |
|||
<StackPanel DockPanel.Dock="Right" |
|||
Margin="16 0 0 0" |
|||
Width="150" |
|||
Spacing="4"> |
|||
<ComboBox ItemsSource="{Binding Orientations}" |
|||
SelectedItem="{Binding Orientation}"/> |
|||
<TextBox Watermark="Item Count" |
|||
UseFloatingWatermark="True" |
|||
Text="{Binding ItemCount}"/> |
|||
<TextBox Watermark="Extent" |
|||
UseFloatingWatermark="True" |
|||
Text="{Binding #listBox.Scroll.Extent, Mode=OneWay}"/> |
|||
<TextBox Watermark="Offset" |
|||
UseFloatingWatermark="True" |
|||
Text="{Binding #listBox.Scroll.Offset, Mode=OneWay}"/> |
|||
<TextBox Watermark="Viewport" |
|||
UseFloatingWatermark="True" |
|||
Text="{Binding #listBox.Scroll.Viewport, Mode=OneWay}"/> |
|||
<TextBlock>Horiz. ScrollBar</TextBlock> |
|||
<ComboBox ItemsSource="{Binding ScrollBarVisibilities}" |
|||
SelectedItem="{Binding HorizontalScrollBarVisibility}"/> |
|||
<TextBlock>Vert. ScrollBar</TextBlock> |
|||
<ComboBox ItemsSource="{Binding ScrollBarVisibilities}" |
|||
SelectedItem="{Binding VerticalScrollBarVisibility}"/> |
|||
<TextBox Watermark="Item to Create" |
|||
UseFloatingWatermark="True" |
|||
Text="{Binding NewItemString}"/> |
|||
<Button Command="{Binding AddItemCommand}">Add Item</Button> |
|||
<Button Command="{Binding RemoveItemCommand}">Remove Item</Button> |
|||
<Button Command="{Binding RecreateCommand}">Recreate</Button> |
|||
<Button Command="{Binding SelectFirstCommand}">Select First</Button> |
|||
<Button Command="{Binding SelectLastCommand}">Select Last</Button> |
|||
<Button Command="{Binding RandomizeSize}">Randomize Size</Button> |
|||
<Button Command="{Binding ResetSize}">Reset Size</Button> |
|||
</StackPanel> |
|||
|
|||
<ListBox Name="listBox" |
|||
ItemsSource="{Binding Items}" |
|||
Selection="{Binding Selection}" |
|||
SelectionMode="Multiple" |
|||
ScrollViewer.HorizontalScrollBarVisibility="{Binding HorizontalScrollBarVisibility, Mode=TwoWay}" |
|||
ScrollViewer.VerticalScrollBarVisibility="{Binding VerticalScrollBarVisibility, Mode=TwoWay}"> |
|||
<ListBox.ItemsPanel> |
|||
<ItemsPanelTemplate> |
|||
<VirtualizingStackPanel Orientation="{Binding Orientation}"/> |
|||
</ItemsPanelTemplate> |
|||
</ListBox.ItemsPanel> |
|||
<ListBox.ItemTemplate> |
|||
<DataTemplate> |
|||
<TextBlock Text="{Binding Header}" Height="{Binding Height}" TextWrapping="Wrap"/> |
|||
</DataTemplate> |
|||
</ListBox.ItemTemplate> |
|||
</ListBox> |
|||
</DockPanel> |
|||
</Window> |
|||
@ -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); |
|||
} |
|||
} |
|||
} |
|||
@ -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<ChatFile>(s, options)!; |
|||
} |
|||
} |
|||
|
|||
public record ChatMessage(string Sender, string Message, DateTimeOffset Timestamp); |
|||
@ -1,15 +1,14 @@ |
|||
using Avalonia; |
|||
|
|||
namespace VirtualizationDemo |
|||
namespace VirtualizationDemo; |
|||
|
|||
class Program |
|||
{ |
|||
class Program |
|||
{ |
|||
public static AppBuilder BuildAvaloniaApp() |
|||
=> AppBuilder.Configure<App>() |
|||
.UsePlatformDetect() |
|||
.LogToTrace(); |
|||
public static AppBuilder BuildAvaloniaApp() |
|||
=> AppBuilder.Configure<App>() |
|||
.UsePlatformDetect() |
|||
.LogToTrace(); |
|||
|
|||
public static int Main(string[] args) |
|||
=> BuildAvaloniaApp().StartWithClassicDesktopLifetime(args); |
|||
} |
|||
public static int Main(string[] args) |
|||
=> BuildAvaloniaApp().StartWithClassicDesktopLifetime(args); |
|||
} |
|||
|
|||
@ -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<ChatMessage>()); |
|||
} |
|||
|
|||
public ObservableCollection<ChatMessage> Messages { get; } |
|||
} |
|||
@ -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); |
|||
} |
|||
} |
|||
@ -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<ExpanderItemViewModel> Items { get; set; } |
|||
} |
|||
@ -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); |
|||
} |
|||
} |
|||
} |
|||
@ -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<ItemViewModel> _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<ItemViewModel> Selection { get; } = new SelectionModel<ItemViewModel>(); |
|||
|
|||
public AvaloniaList<ItemViewModel> 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<Orientation> Orientations => |
|||
Enum.GetValues(typeof(Orientation)).Cast<Orientation>(); |
|||
|
|||
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<ScrollBarVisibility> ScrollBarVisibilities => |
|||
Enum.GetValues(typeof(ScrollBarVisibility)).Cast<ScrollBarVisibility>(); |
|||
|
|||
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<ItemViewModel>(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<ItemViewModel>(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(); |
|||
} |
|||
|
|||
@ -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); |
|||
} |
|||
} |
|||
@ -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<PlaygroundItemViewModel> 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<PlaygroundItemViewModel> 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; |
|||
} |
|||
} |
|||
@ -0,0 +1,39 @@ |
|||
<UserControl xmlns="https://github.com/avaloniaui" |
|||
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" |
|||
xmlns:d="http://schemas.microsoft.com/expression/blend/2008" |
|||
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" |
|||
xmlns:vm="using:VirtualizationDemo.ViewModels" |
|||
mc:Ignorable="d" d:DesignWidth="800" d:DesignHeight="450" |
|||
x:Class="VirtualizationDemo.Views.ChatPageView" |
|||
x:DataType="vm:ChatPageViewModel"> |
|||
<ListBox ItemsSource="{Binding Messages}"> |
|||
<ListBox.ItemContainerTheme> |
|||
<ControlTheme TargetType="ListBoxItem" BasedOn="{StaticResource {x:Type ListBoxItem}}"> |
|||
<Setter Property="Padding" Value="8"/> |
|||
</ControlTheme> |
|||
</ListBox.ItemContainerTheme> |
|||
<ListBox.ItemTemplate> |
|||
<DataTemplate> |
|||
<Border CornerRadius="8" |
|||
Background="{DynamicResource SystemControlBackgroundAltHighBrush}" |
|||
TextElement.Foreground="{DynamicResource SystemControlForegroundBaseHighBrush}" |
|||
Padding="6" |
|||
HorizontalAlignment="Left" |
|||
MaxWidth="280"> |
|||
<DockPanel> |
|||
<TextBlock DockPanel.Dock="Top" |
|||
Text="{Binding Sender}" |
|||
FontWeight="Bold"/> |
|||
<TextBlock DockPanel.Dock="Bottom" |
|||
Text="{Binding Timestamp}" |
|||
FontSize="10" |
|||
Foreground="{DynamicResource SystemControlForegroundBaseMediumBrush}" |
|||
TextAlignment="Right" |
|||
Margin="0 4 0 0"/> |
|||
<TextBlock Text="{Binding Message}" TextWrapping="Wrap"/> |
|||
</DockPanel> |
|||
</Border> |
|||
</DataTemplate> |
|||
</ListBox.ItemTemplate> |
|||
</ListBox> |
|||
</UserControl> |
|||
@ -0,0 +1,11 @@ |
|||
using Avalonia.Controls; |
|||
|
|||
namespace VirtualizationDemo.Views; |
|||
|
|||
public partial class ChatPageView : UserControl |
|||
{ |
|||
public ChatPageView() |
|||
{ |
|||
InitializeComponent(); |
|||
} |
|||
} |
|||
@ -0,0 +1,18 @@ |
|||
<UserControl xmlns="https://github.com/avaloniaui" |
|||
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" |
|||
xmlns:d="http://schemas.microsoft.com/expression/blend/2008" |
|||
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" |
|||
xmlns:vm="using:VirtualizationDemo.ViewModels" |
|||
mc:Ignorable="d" d:DesignWidth="800" d:DesignHeight="450" |
|||
x:Class="VirtualizationDemo.Views.ExpanderPageView" |
|||
x:DataType="vm:ExpanderPageViewModel"> |
|||
<ListBox ItemsSource="{Binding Items}"> |
|||
<ListBox.ItemTemplate> |
|||
<DataTemplate> |
|||
<Expander Header="{Binding Header}" IsExpanded="{Binding IsExpanded}"> |
|||
<Border Width="200" Height="300"/> |
|||
</Expander> |
|||
</DataTemplate> |
|||
</ListBox.ItemTemplate> |
|||
</ListBox> |
|||
</UserControl> |
|||
@ -0,0 +1,13 @@ |
|||
using Avalonia; |
|||
using Avalonia.Controls; |
|||
using Avalonia.Markup.Xaml; |
|||
|
|||
namespace VirtualizationDemo.Views; |
|||
|
|||
public partial class ExpanderPageView : UserControl |
|||
{ |
|||
public ExpanderPageView() |
|||
{ |
|||
InitializeComponent(); |
|||
} |
|||
} |
|||
@ -0,0 +1,66 @@ |
|||
<UserControl xmlns="https://github.com/avaloniaui" |
|||
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" |
|||
xmlns:d="http://schemas.microsoft.com/expression/blend/2008" |
|||
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" |
|||
xmlns:vm="using:VirtualizationDemo.ViewModels" |
|||
mc:Ignorable="d" d:DesignWidth="800" d:DesignHeight="450" |
|||
x:Class="VirtualizationDemo.Views.PlaygroundPageView" |
|||
x:DataType="vm:PlaygroundPageViewModel"> |
|||
<DockPanel> |
|||
<StackPanel DockPanel.Dock="Right" Margin="8 0" Width="200"> |
|||
<DropDownButton Content="Selection" HorizontalAlignment="Stretch"> |
|||
<Button.Flyout> |
|||
<Flyout> |
|||
<StackPanel> |
|||
<CheckBox IsChecked="{Binding Multiple}">Multiple</CheckBox> |
|||
<CheckBox IsChecked="{Binding Toggle}">Toggle</CheckBox> |
|||
<CheckBox IsChecked="{Binding AlwaysSelected}">AlwaysSelected</CheckBox> |
|||
<CheckBox IsChecked="{Binding #list.AutoScrollToSelectedItem}">AutoScrollToSelectedItem</CheckBox> |
|||
<CheckBox IsChecked="{Binding #list.WrapSelection}">WrapSelection</CheckBox> |
|||
</StackPanel> |
|||
</Flyout> |
|||
</Button.Flyout> |
|||
</DropDownButton> |
|||
|
|||
<Label>_Select Item</Label> |
|||
<DockPanel> |
|||
<TextBox x:Name="scrollToIndex" Text="{Binding ScrollToIndex}"> |
|||
<TextBox.InnerRightContent> |
|||
<StackPanel Orientation="Horizontal"> |
|||
<Button DockPanel.Dock="Right" |
|||
Command="{Binding RandomizeScrollToIndex}" |
|||
ToolTip.Tip="Randomize"> |
|||
⟳ |
|||
</Button> |
|||
<Button DockPanel.Dock="Right" |
|||
Command="{Binding ExecuteScrollToIndex}" |
|||
ToolTip.Tip="Execute"> |
|||
⮐ |
|||
</Button> |
|||
</StackPanel> |
|||
</TextBox.InnerRightContent> |
|||
</TextBox> |
|||
</DockPanel> |
|||
|
|||
<Label>New Item</Label> |
|||
<TextBox Text="{Binding NewItemHeader}"> |
|||
<TextBox.InnerRightContent> |
|||
<Button Command="{Binding AddAtSelectedIndex}" |
|||
ToolTip.Tip="Add at Selected Index">+</Button> |
|||
</TextBox.InnerRightContent> |
|||
</TextBox> |
|||
|
|||
<Button Command="{Binding DeleteSelectedItem}" Margin="0 8 0 0"> |
|||
Delete Selected |
|||
</Button> |
|||
</StackPanel> |
|||
|
|||
<TextBlock Name="itemCount" DockPanel.Dock="Bottom"/> |
|||
|
|||
<ListBox Name="list" |
|||
ItemsSource="{Binding Items}" |
|||
DisplayMemberBinding="{Binding Header}" |
|||
Selection="{Binding Selection}" |
|||
SelectionMode="{Binding SelectionMode}"/> |
|||
</DockPanel> |
|||
</UserControl> |
|||
@ -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; |
|||
} |
|||
} |
|||
@ -1,19 +1,24 @@ |
|||
<Project Sdk="Microsoft.NET.Sdk"> |
|||
<PropertyGroup> |
|||
<OutputType>Exe</OutputType> |
|||
<OutputType>WinExe</OutputType> |
|||
<TargetFramework>net6.0</TargetFramework> |
|||
<IncludeAvaloniaGenerators>true</IncludeAvaloniaGenerators> |
|||
</PropertyGroup> |
|||
<ItemGroup> |
|||
<ProjectReference Include="..\..\packages\Avalonia\Avalonia.csproj" /> |
|||
<ProjectReference Include="..\..\src\Avalonia.Desktop\Avalonia.Desktop.csproj" /> |
|||
<ProjectReference Include="..\..\src\Avalonia.Diagnostics\Avalonia.Diagnostics.csproj" /> |
|||
<ProjectReference Include="..\..\src\Avalonia.Themes.Fluent\Avalonia.Themes.Fluent.csproj" /> |
|||
<ProjectReference Include="..\..\src\Avalonia.Fonts.Inter\Avalonia.Fonts.Inter.csproj" /> |
|||
<ProjectReference Include="..\..\src\Avalonia.Themes.Simple\Avalonia.Themes.Simple.csproj" /> |
|||
<ProjectReference Include="..\..\src\Linux\Avalonia.LinuxFramebuffer\Avalonia.LinuxFramebuffer.csproj" /> |
|||
<ProjectReference Include="..\MiniMvvm\MiniMvvm.csproj" /> |
|||
<ProjectReference Include="..\SampleControls\ControlSamples.csproj" /> |
|||
</ItemGroup> |
|||
<ItemGroup> |
|||
<None Update="Assets\chat.json"> |
|||
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory> |
|||
</None> |
|||
</ItemGroup> |
|||
<Import Project="..\..\build\SampleApp.props" /> |
|||
<Import Project="..\..\build\EmbedXaml.props" /> |
|||
<Import Project="..\..\build\Rx.props" /> |
|||
<Import Condition="'$(TargetFramework)'=='net461'" Project="..\..\build\NetFX.props" /> |
|||
<Import Project="..\..\build\ReferenceCoreLibraries.props" /> |
|||
<Import Project="..\..\build\BuildTargets.targets" /> |
|||
<Import Project="..\..\build\SourceGenerators.props" /> |
|||
<Import Project="..\..\build\NullableEnable.props" /> |
|||
</Project> |
|||
|
|||
@ -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<string, ConcurrentDictionary<FontCollectionKey, IGlyphTypeface?>> _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<FontFamily> 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<FontCollectionKey, |
|||
IGlyphTypeface?> 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<FontCollectionKey, |
|||
IGlyphTypeface?> 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<FontCollectionKey, |
|||
IGlyphTypeface?> 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; |
|||
} |
|||
} |
|||
} |
|||
@ -0,0 +1,499 @@ |
|||
using System; |
|||
using System.Collections.Generic; |
|||
using Avalonia.Utilities; |
|||
|
|||
namespace Avalonia.Controls.Utils |
|||
{ |
|||
/// <summary>
|
|||
/// Stores the realized element state for a virtualizing panel that arranges its children
|
|||
/// in a stack layout, such as <see cref="VirtualizingStackPanel"/>.
|
|||
/// </summary>
|
|||
internal class RealizedStackElements |
|||
{ |
|||
private int _firstIndex; |
|||
private List<Control?>? _elements; |
|||
private List<double>? _sizes; |
|||
private double _startU; |
|||
private bool _startUUnstable; |
|||
|
|||
/// <summary>
|
|||
/// Gets the number of realized elements.
|
|||
/// </summary>
|
|||
public int Count => _elements?.Count ?? 0; |
|||
|
|||
/// <summary>
|
|||
/// Gets the index of the first realized element, or -1 if no elements are realized.
|
|||
/// </summary>
|
|||
public int FirstIndex => _elements?.Count > 0 ? _firstIndex : -1; |
|||
|
|||
/// <summary>
|
|||
/// Gets the index of the last realized element, or -1 if no elements are realized.
|
|||
/// </summary>
|
|||
public int LastIndex => _elements?.Count > 0 ? _firstIndex + _elements.Count - 1 : -1; |
|||
|
|||
/// <summary>
|
|||
/// Gets the elements.
|
|||
/// </summary>
|
|||
public IReadOnlyList<Control?> Elements => _elements ??= new List<Control?>(); |
|||
|
|||
/// <summary>
|
|||
/// Gets the sizes of the elements on the primary axis.
|
|||
/// </summary>
|
|||
public IReadOnlyList<double> SizeU => _sizes ??= new List<double>(); |
|||
|
|||
/// <summary>
|
|||
/// Gets the position of the first element on the primary axis.
|
|||
/// </summary>
|
|||
public double StartU => _startU; |
|||
|
|||
/// <summary>
|
|||
/// Adds a newly realized element to the collection.
|
|||
/// </summary>
|
|||
/// <param name="index">The index of the element.</param>
|
|||
/// <param name="element">The element.</param>
|
|||
/// <param name="u">The position of the elemnt on the primary axis.</param>
|
|||
/// <param name="sizeU">The size of the element on the primary axis.</param>
|
|||
public void Add(int index, Control element, double u, double sizeU) |
|||
{ |
|||
if (index < 0) |
|||
throw new ArgumentOutOfRangeException(nameof(index)); |
|||
|
|||
_elements ??= new List<Control?>(); |
|||
_sizes ??= new List<double>(); |
|||
|
|||
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."); |
|||
} |
|||
} |
|||
|
|||
/// <summary>
|
|||
/// Gets the element at the specified index, if realized.
|
|||
/// </summary>
|
|||
/// <param name="index">The index in the source collection of the element to get.</param>
|
|||
/// <returns>The element if realized; otherwise null.</returns>
|
|||
public Control? GetElement(int index) |
|||
{ |
|||
var i = index - FirstIndex; |
|||
if (i >= 0 && i < _elements?.Count) |
|||
return _elements[i]; |
|||
return null; |
|||
} |
|||
|
|||
/// <summary>
|
|||
/// Gets or estimates the index and start U position of the anchor element for the
|
|||
/// specified viewport.
|
|||
/// </summary>
|
|||
/// <param name="viewportStartU">The U position of the start of the viewport.</param>
|
|||
/// <param name="viewportEndU">The U position of the end of the viewport.</param>
|
|||
/// <param name="itemCount">The number of items in the list.</param>
|
|||
/// <param name="estimatedElementSizeU">The current estimated element size.</param>
|
|||
/// <returns>
|
|||
/// 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
|
|||
/// </returns>
|
|||
/// <remarks>
|
|||
/// 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.
|
|||
/// </remarks>
|
|||
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); |
|||
} |
|||
|
|||
/// <summary>
|
|||
/// Gets the position of the element with the requested index on the primary axis, if realized.
|
|||
/// </summary>
|
|||
/// <returns>
|
|||
/// The position of the element, or NaN if the element is not realized.
|
|||
/// </returns>
|
|||
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; |
|||
} |
|||
|
|||
/// <summary>
|
|||
/// Estimates the average U size of all elements in the source collection based on the
|
|||
/// realized elements.
|
|||
/// </summary>
|
|||
/// <returns>
|
|||
/// The estimated U size of an element, or -1 if not enough information is present to make
|
|||
/// an estimate.
|
|||
/// </returns>
|
|||
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; |
|||
} |
|||
|
|||
/// <summary>
|
|||
/// Gets the index of the specified element.
|
|||
/// </summary>
|
|||
/// <param name="element">The element.</param>
|
|||
/// <returns>The index or -1 if the element is not present in the collection.</returns>
|
|||
public int GetIndex(Control element) |
|||
{ |
|||
return _elements?.IndexOf(element) is int index && index >= 0 ? index + FirstIndex : -1; |
|||
} |
|||
|
|||
/// <summary>
|
|||
/// Updates the elements in response to items being inserted into the source collection.
|
|||
/// </summary>
|
|||
/// <param name="index">The index in the source collection of the insert.</param>
|
|||
/// <param name="count">The number of items inserted.</param>
|
|||
/// <param name="updateElementIndex">A method used to update the element indexes.</param>
|
|||
public void ItemsInserted(int index, int count, Action<Control, int, int> 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); |
|||
} |
|||
} |
|||
} |
|||
|
|||
/// <summary>
|
|||
/// Updates the elements in response to items being removed from the source collection.
|
|||
/// </summary>
|
|||
/// <param name="index">The index in the source collection of the remove.</param>
|
|||
/// <param name="count">The number of items removed.</param>
|
|||
/// <param name="updateElementIndex">A method used to update the element indexes.</param>
|
|||
/// <param name="recycleElement">A method used to recycle elements.</param>
|
|||
public void ItemsRemoved( |
|||
int index, |
|||
int count, |
|||
Action<Control, int, int> updateElementIndex, |
|||
Action<Control> 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; |
|||
} |
|||
} |
|||
} |
|||
|
|||
/// <summary>
|
|||
/// Recycles all elements in response to the source collection being reset.
|
|||
/// </summary>
|
|||
/// <param name="recycleElement">A method used to recycle elements.</param>
|
|||
public void ItemsReset(Action<Control> 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(); |
|||
|
|||
} |
|||
|
|||
/// <summary>
|
|||
/// Recycles elements before a specific index.
|
|||
/// </summary>
|
|||
/// <param name="index">The index in the source collection of new first element.</param>
|
|||
/// <param name="recycleElement">A method used to recycle elements.</param>
|
|||
public void RecycleElementsBefore(int index, Action<Control, int> 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; |
|||
} |
|||
} |
|||
|
|||
/// <summary>
|
|||
/// Recycles elements after a specific index.
|
|||
/// </summary>
|
|||
/// <param name="index">The index in the source collection of new last element.</param>
|
|||
/// <param name="recycleElement">A method used to recycle elements.</param>
|
|||
public void RecycleElementsAfter(int index, Action<Control, int> 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); |
|||
} |
|||
} |
|||
|
|||
/// <summary>
|
|||
/// Recycles all realized elements.
|
|||
/// </summary>
|
|||
/// <param name="recycleElement">A method used to recycle elements.</param>
|
|||
public void RecycleAllElements(Action<Control, int> 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(); |
|||
} |
|||
|
|||
/// <summary>
|
|||
/// Resets the element list and prepares it for reuse.
|
|||
/// </summary>
|
|||
public void ResetForReuse() |
|||
{ |
|||
_startU = _firstIndex = 0; |
|||
_startUUnstable = false; |
|||
_elements?.Clear(); |
|||
_sizes?.Clear(); |
|||
} |
|||
} |
|||
|
|||
} |
|||
File diff suppressed because it is too large
File diff suppressed because it is too large
@ -0,0 +1,51 @@ |
|||
using System.Threading.Tasks; |
|||
using Avalonia.Controls; |
|||
using Avalonia.Media; |
|||
using Xunit; |
|||
#pragma warning disable CS0649
|
|||
|
|||
#if AVALONIA_SKIA
|
|||
namespace Avalonia.Skia.RenderTests; |
|||
|
|||
public class BoxShadowTests : TestBase |
|||
{ |
|||
|
|||
public BoxShadowTests() : base(@"Media\BoxShadow") |
|||
{ |
|||
} |
|||
|
|||
[Fact] |
|||
public async Task BoxShadowShouldBeRenderedEvenWithNullBrushAndPen() |
|||
{ |
|||
var target = new Border |
|||
{ |
|||
Width = 200, |
|||
Height = 200, |
|||
Background = null, |
|||
Child = new Border() |
|||
{ |
|||
Background = null, |
|||
Margin = new Thickness(40), |
|||
BoxShadow = new BoxShadows(new BoxShadow |
|||
{ |
|||
Blur = 0, |
|||
Color = Colors.Blue, |
|||
OffsetX = 10, |
|||
OffsetY = 15, |
|||
Spread = 0 |
|||
}), |
|||
Child = new Border |
|||
{ |
|||
Background = Brushes.Red |
|||
} |
|||
} |
|||
}; |
|||
|
|||
await RenderToFile(target); |
|||
CompareImages(); |
|||
} |
|||
|
|||
|
|||
} |
|||
|
|||
#endif
|
|||
|
After Width: | Height: | Size: 533 B |
Loading…
Reference in new issue