Merge branch 'master' into fixes/DevTools/WithoutApplicationLifetime

This commit is contained in:
workgroupengineering
2023-04-14 17:59:28 +02:00
committed by GitHub
572 changed files with 18143 additions and 9890 deletions
@@ -0,0 +1,5 @@
<ProjectConfiguration>
<Settings>
<IgnoreThisComponentCompletely>True</IgnoreThisComponentCompletely>
</Settings>
</ProjectConfiguration>
@@ -0,0 +1,5 @@
<ProjectConfiguration>
<Settings>
<IgnoreThisComponentCompletely>True</IgnoreThisComponentCompletely>
</Settings>
</ProjectConfiguration>
+4 -3
View File
@@ -39,13 +39,14 @@
"src\\Markup\\Avalonia.Markup.Xaml\\Avalonia.Markup.Xaml.csproj",
"src\\Markup\\Avalonia.Markup\\Avalonia.Markup.csproj",
"src\\Skia\\Avalonia.Skia\\Avalonia.Skia.csproj",
"src\\Windows\\Avalonia.Direct2D1\\Avalonia.Direct2D1.csproj",
"src\\Windows\\Avalonia.Win32.Interop\\Avalonia.Win32.Interop.csproj",
"src\\Windows\\Avalonia.Win32\\Avalonia.Win32.csproj",
"src\\tools\\Avalonia.Generators\\Avalonia.Generators.csproj",
"src\\tools\\Avalonia.Generators\\Avalonia.Generators.csproj",
"src\\tools\\DevAnalyzers\\DevAnalyzers.csproj",
"src\\tools\\DevGenerators\\DevGenerators.csproj",
"src\\tools\\PublicAnalyzers\\Avalonia.Analyzers.csproj",
"src\\Windows\\Avalonia.Direct2D1\\Avalonia.Direct2D1.csproj",
"src\\Windows\\Avalonia.Win32.Interop\\Avalonia.Win32.Interop.csproj",
"src\\Windows\\Avalonia.Win32\\Avalonia.Win32.csproj",
"tests\\Avalonia.Base.UnitTests\\Avalonia.Base.UnitTests.csproj",
"tests\\Avalonia.Benchmarks\\Avalonia.Benchmarks.csproj",
"tests\\Avalonia.Controls.DataGrid.UnitTests\\Avalonia.Controls.DataGrid.UnitTests.csproj",
+7 -12
View File
@@ -598,13 +598,13 @@
{
[[_markedText mutableString] setString:@""];
[[self inputContext] discardMarkedText];
if(!_parent->InputMethod->IsActive()){
return;
}
_parent->InputMethod->Client->SetPreeditText(nullptr);
[[self inputContext] discardMarkedText];
}
- (NSArray<NSString *> *)validAttributesForMarkedText
@@ -619,17 +619,12 @@
- (void)insertText:(id)string replacementRange:(NSRange)replacementRange
{
//[_text replaceCharactersInRange:replacementRange withString:string];
[self unmarkText];
//if(!_lastKeyHandled)
//{
if(_parent != nullptr)
{
_lastKeyHandled = _parent->BaseEvents->RawTextInputEvent(0, [string UTF8String]);
}
//}
if(_parent != nullptr)
{
_lastKeyHandled = _parent->BaseEvents->RawTextInputEvent(0, [string UTF8String]);
}
[[self inputContext] invalidateCharacterCoordinates];
}
+7 -3
View File
@@ -281,10 +281,13 @@ HRESULT WindowImpl::SetDecorations(SystemDecorations value) {
case SystemDecorationsFull:
[Window setHasShadow:YES];
[Window setTitleVisibility:NSWindowTitleVisible];
[Window setTitlebarAppearsTransparent:NO];
[Window setTitle:_lastTitle];
if (!_isClientAreaExtended) {
[Window setTitleVisibility:NSWindowTitleVisible];
[Window setTitlebarAppearsTransparent:NO];
}
if (currentWindowState == Maximized) {
auto newFrame = [Window contentRectForFrameRect:[Window frame]].size;
@@ -611,7 +614,8 @@ void WindowImpl::UpdateStyle() {
}
bool wantsChrome = (_extendClientHints & AvnSystemChrome) || (_extendClientHints & AvnPreferSystemChrome);
bool hasTrafficLights = _isClientAreaExtended ? wantsChrome : _decorations == SystemDecorationsFull;
bool hasTrafficLights = (_decorations == SystemDecorationsFull) &&
(_isClientAreaExtended ? wantsChrome : true);
NSButton* closeButton = [Window standardWindowButton:NSWindowCloseButton];
NSButton* miniaturizeButton = [Window standardWindowButton:NSWindowMiniaturizeButton];
@@ -1,193 +1,266 @@
#include "common.h"
class PlatformThreadingInterface;
class LoopCancellation : public ComSingleObject<IAvnLoopCancellation, &IID_IAvnLoopCancellation>
{
public:
FORWARD_IUNKNOWN()
bool Running = false;
bool Cancelled = false;
bool IsApp = false;
virtual void Cancel() override
{
Cancelled = true;
if(Running)
{
Running = false;
if(![NSThread isMainThread])
{
AddRef();
dispatch_async(dispatch_get_main_queue(), ^{
if(Release() == 0)
return;
Cancel();
});
return;
};
if(IsApp)
[NSApp stop:nil];
else
{
// Wakeup the event loop
NSEvent* event = [NSEvent otherEventWithType:NSEventTypeApplicationDefined
location:NSMakePoint(0, 0)
modifierFlags:0
timestamp:0
windowNumber:0
context:nil
subtype:0
data1:0
data2:0];
[NSApp postEvent:event atStart:YES];
}
}
};
};
// CFRunLoopTimerSetNextFireDate docs recommend to "create a repeating timer with an initial
// firing time in the distant future (or the initial firing time) and a very large repeat
// interval—on the order of decades or more"
static double distantFutureInterval = (double)50*365*24*3600;
@interface Signaler : NSObject
-(void) setParent: (PlatformThreadingInterface*)parent;
-(void) signal: (int) priority;
-(void) setEvents:(IAvnPlatformThreadingInterfaceEvents*) events;
-(void) updateTimer:(int)ms;
-(Signaler*) init;
-(void) destroyObserver;
-(void) signal;
@end
@implementation ActionCallback
@implementation Signaler
{
ComPtr<IAvnActionCallback> _callback;
ComPtr<IAvnPlatformThreadingInterfaceEvents> _events;
bool _wakeupDelegateSent;
bool _signaled;
bool _backgroundProcessingRequested;
CFRunLoopObserverRef _observer;
CFRunLoopTimerRef _timer;
}
- (ActionCallback*) initWithCallback: (IAvnActionCallback*) callback
- (void) checkSignaled
{
_callback = callback;
bool signaled;
@synchronized (self) {
signaled = _signaled;
_signaled = false;
}
if(signaled)
{
_events->Signaled();
}
}
- (Signaler*) init
{
_observer = CFRunLoopObserverCreateWithHandler(nil,
kCFRunLoopBeforeSources
| kCFRunLoopAfterWaiting
| kCFRunLoopBeforeWaiting
,
true, 0,
^(CFRunLoopObserverRef observer, CFRunLoopActivity activity) {
if(activity == kCFRunLoopBeforeWaiting)
{
bool triggerProcessing;
@synchronized (self) {
triggerProcessing = self->_backgroundProcessingRequested;
self->_backgroundProcessingRequested = false;
}
if(triggerProcessing)
self->_events->ReadyForBackgroundProcessing();
}
[self checkSignaled];
});
CFRunLoopAddObserver(CFRunLoopGetMain(), _observer, kCFRunLoopCommonModes);
_timer = CFRunLoopTimerCreateWithHandler(nil, CFAbsoluteTimeGetCurrent() + distantFutureInterval, distantFutureInterval, 0, 0, ^(CFRunLoopTimerRef timer) {
self->_events->Timer();
});
CFRunLoopAddTimer(CFRunLoopGetMain(), _timer, kCFRunLoopCommonModes);
return self;
}
- (void) action
- (void) destroyObserver
{
_callback->Run();
if(_observer != nil)
{
CFRunLoopObserverInvalidate(_observer);
CFRelease(_observer);
_observer = nil;
}
if(_timer != nil)
{
CFRunLoopTimerInvalidate(_timer);
CFRelease(_timer);
_timer = nil;
}
}
-(void) updateTimer:(int)ms
{
if(_timer == nil)
return;
double interval = ms < 0 ? distantFutureInterval : ((double)ms / 1000);
CFRunLoopTimerSetTolerance(_timer, 0);
CFRunLoopTimerSetNextFireDate(_timer, CFAbsoluteTimeGetCurrent() + interval);
}
- (void) setEvents: (IAvnPlatformThreadingInterfaceEvents*) events
{
_events = events;
}
- (void) signal
{
@synchronized (self) {
if(_signaled)
return;
_signaled = true;
dispatch_async(dispatch_get_main_queue(), ^{
[self checkSignaled];
});
CFRunLoopWakeUp(CFRunLoopGetMain());
}
}
- (void) requestBackgroundProcessing
{
@synchronized (self) {
if(_backgroundProcessingRequested)
return;
_backgroundProcessingRequested = true;
dispatch_async(dispatch_get_main_queue(), ^{
// This is needed to wakeup the loop if we are called from inside of BeforeWait hook
});
}
}
@end
class TimerWrapper : public ComUnknownObject
{
NSTimer* _timer;
public:
TimerWrapper(IAvnActionCallback* callback, int ms)
{
auto cb = [[ActionCallback alloc] initWithCallback:callback];
_timer = [NSTimer scheduledTimerWithTimeInterval:(NSTimeInterval)(double)ms/1000 target:cb selector:@selector(action) userInfo:nullptr repeats:true];
}
virtual ~TimerWrapper()
{
[_timer invalidate];
}
};
class PlatformThreadingInterface : public ComSingleObject<IAvnPlatformThreadingInterface, &IID_IAvnPlatformThreadingInterface>
{
private:
ComPtr<IAvnPlatformThreadingInterfaceEvents> _events;
Signaler* _signaler;
bool _wasRunningAtLeastOnce = false;
class LoopCancellation : public ComSingleObject<IAvnLoopCancellation, &IID_IAvnLoopCancellation>
{
public:
FORWARD_IUNKNOWN()
bool Running = false;
bool Cancelled = false;
virtual void Cancel() override
{
Cancelled = true;
if(Running)
{
Running = false;
dispatch_async(dispatch_get_main_queue(), ^{
[[NSApplication sharedApplication] stop:nil];
NSEvent* event = [NSEvent otherEventWithType:NSEventTypeApplicationDefined
location:NSMakePoint(0, 0)
modifierFlags:0
timestamp:0
windowNumber:0
context:nil
subtype:0
data1:0
data2:0];
[NSApp postEvent:event atStart:YES];
});
}
}
};
CFRunLoopObserverRef _observer = nil;
public:
FORWARD_IUNKNOWN()
ComPtr<IAvnSignaledCallback> SignaledCallback;
PlatformThreadingInterface()
{
_signaler = [Signaler new];
[_signaler setParent:this];
}
};
~PlatformThreadingInterface()
{
if(_signaler)
[_signaler setParent: NULL];
_signaler = NULL;
[_signaler destroyObserver];
}
virtual bool GetCurrentThreadIsLoopThread() override
bool GetCurrentThreadIsLoopThread() override
{
return [NSThread isMainThread];
}
virtual void SetSignaledCallback(IAvnSignaledCallback* cb) override
};
void SetEvents(IAvnPlatformThreadingInterfaceEvents *cb) override
{
SignaledCallback = cb;
}
virtual IAvnLoopCancellation* CreateLoopCancellation() override
_events = cb;
[_signaler setEvents:cb];
};
IAvnLoopCancellation *CreateLoopCancellation() override
{
return new LoopCancellation();
}
};
virtual HRESULT RunLoop(IAvnLoopCancellation* cancel) override
void RunLoop(IAvnLoopCancellation *cancel) override
{
START_COM_CALL;
auto can = dynamic_cast<LoopCancellation*>(cancel);
if(can->Cancelled)
return S_OK;
if(_wasRunningAtLeastOnce)
return E_FAIL;
can->Running = true;
_wasRunningAtLeastOnce = true;
[NSApp run];
return S_OK;
}
virtual void Signal(int priority) override
{
[_signaler signal:priority];
}
virtual IUnknown* StartTimer(int priority, int ms, IAvnActionCallback* callback) override
{
@autoreleasepool {
return new TimerWrapper(callback, ms);
}
}
};
@implementation Signaler
PlatformThreadingInterface* _parent = 0;
bool _signaled = 0;
NSArray<NSString*>* _modes;
-(Signaler*) init
{
if(self = [super init])
{
_modes = [NSArray arrayWithObjects: NSDefaultRunLoopMode, NSEventTrackingRunLoopMode, NSModalPanelRunLoopMode, NSRunLoopCommonModes, NSConnectionReplyMode, nil];
}
return self;
}
-(void) perform
{
ComPtr<IAvnSignaledCallback> cb;
@synchronized (self) {
_signaled = false;
if(_parent != NULL)
cb = _parent->SignaledCallback;
}
if(cb != nullptr)
cb->Signaled(0, false);
}
-(void) setParent:(PlatformThreadingInterface *)parent
{
@synchronized (self) {
_parent = parent;
}
}
-(void) signal: (int) priority
{
@synchronized (self) {
if(_signaled)
return;
_signaled = true;
[self performSelector:@selector(perform) onThread:[NSThread mainThread] withObject:NULL waitUntilDone:false modes:_modes];
can->Running = true;
if(![NSApp isRunning])
{
can->IsApp = true;
[NSApp run];
return;
}
else
{
while(!can->Cancelled)
{
@autoreleasepool
{
NSEvent* ev = [NSApp
nextEventMatchingMask:NSEventMaskAny
untilDate: [NSDate dateWithTimeIntervalSinceNow:1]
inMode:NSDefaultRunLoopMode
dequeue:true];
if(ev != NULL)
[NSApp sendEvent:ev];
}
}
}
};
void Signal() override
{
[_signaler signal];
};
void UpdateTimer(int ms) override
{
[_signaler updateTimer:ms];
};
void RequestBackgroundProcessing() override {
[_signaler requestBackgroundProcessing];
}
}
@end
};
extern IAvnPlatformThreadingInterface* CreatePlatformThreading()
{
+4
View File
@@ -43,6 +43,10 @@
<Pack>true</Pack>
<PackagePath>build\;buildTransitive\</PackagePath>
</Content>
<Content Include="..\..\src\tools\Avalonia.Generators\Avalonia.Generators.props">
<Pack>true</Pack>
<PackagePath>build\;buildTransitive\</PackagePath>
</Content>
<Content Include="*.targets">
<Pack>true</Pack>
<PackagePath>build\;buildTransitive\</PackagePath>
+1
View File
@@ -6,6 +6,7 @@
<AvaloniaUseExternalMSBuild>false</AvaloniaUseExternalMSBuild>
</PropertyGroup>
<Import Project="$(MSBuildThisFileDirectory)\AvaloniaBuildTasks.props"/>
<Import Project="$(MSBuildThisFileDirectory)\Avalonia.Generators.props"/>
<!-- Allow loading the AvaloniaVS extension when referencing the Avalonia nuget package -->
<ItemGroup>
+4
View File
@@ -5,6 +5,10 @@
<br />
[![NuGet](https://img.shields.io/nuget/v/Avalonia.svg)](https://www.nuget.org/packages/Avalonia) [![downloads](https://img.shields.io/nuget/dt/avalonia)](https://www.nuget.org/packages/Avalonia) ![Size](https://img.shields.io/github/repo-size/avaloniaui/avalonia.svg)
# ⚠️ **v11 Update - Pausing community contributions**
for more information see [this](https://github.com/AvaloniaUI/Avalonia/discussions/10599) discussion.
## 📖 About
Avalonia is a cross-platform UI framework for dotnet, providing a flexible styling system and supporting a wide range of Operating Systems such as Windows, Linux, macOS. Avalonia is mature and production ready. We also have in beta release support for iOS, Android and in early stages support for browser via WASM.
+2 -2
View File
@@ -75,11 +75,11 @@
</StackPanel.DataTemplates>
<StackPanel Margin="18" Spacing="4" Width="200">
<TextBlock FontSize="16" Text="Multiple"/>
<ListBox Items="{Binding Items}" SelectionMode="Multiple" Selection="{Binding Selection}"/>
<ListBox ItemsSource="{Binding Items}" SelectionMode="Multiple" Selection="{Binding Selection}"/>
</StackPanel>
<StackPanel Margin="18" Spacing="4" Width="200">
<TextBlock FontSize="16" Text="Multiple"/>
<ListBox Items="{Binding Items}" SelectionMode="Multiple" Selection="{Binding Selection}"/>
<ListBox ItemsSource="{Binding Items}" SelectionMode="Multiple" Selection="{Binding Selection}"/>
</StackPanel>
<ContentControl Content="{ReflectionBinding Selection.SelectedItems[0]}">
<ContentControl.DataTemplates>
@@ -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
{
}
@@ -2,4 +2,6 @@
<manifest xmlns:android="http://schemas.android.com/apk/res/android" android:installLocation="auto">
<application android:label="ControlCatalog.Android" android:icon="@drawable/Icon"></application>
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" />
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE " />
</manifest>
@@ -0,0 +1,4 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<color name="splash_background">#212121</color>
</resources>
+1 -4
View File
@@ -147,9 +147,6 @@
<TabItem Header="ScrollViewer">
<pages:ScrollViewerPage />
</TabItem>
<TabItem Header="ScrollViewer Snapping">
<pages:ScrollSnapPage />
</TabItem>
<TabItem Header="Slider">
<pages:SliderPage />
</TabItem>
@@ -241,7 +238,7 @@
</ComboBox.Items>
</ComboBox>
<ComboBox HorizontalAlignment="Stretch"
Items="{Binding WindowStates}"
ItemsSource="{Binding WindowStates}"
SelectedItem="{Binding WindowState}" />
</StackPanel>
</Flyout>
@@ -1,16 +1,23 @@
using System;
using System.Collections.Generic;
using System.Linq;
using Avalonia;
using Avalonia.Controls;
using Avalonia.Controls.Notifications;
using Avalonia.Input;
using Avalonia.Interactivity;
using Avalonia.Markup.Xaml;
using Avalonia.Platform;
using Avalonia.Platform.Storage;
using Avalonia.Platform.Storage.FileIO;
namespace ControlCatalog.Pages
{
public partial class ClipboardPage : UserControl
{
private INotificationManager? _notificationManager;
private INotificationManager NotificationManager => _notificationManager
??= new WindowNotificationManager(TopLevel.GetTopLevel(this)!);
public ClipboardPage()
{
InitializeComponent();
@@ -25,13 +32,13 @@ namespace ControlCatalog.Pages
private async void CopyText(object? sender, RoutedEventArgs args)
{
if (Application.Current!.Clipboard is { } clipboard && ClipboardContent is { } clipboardContent)
if (TopLevel.GetTopLevel(this)?.Clipboard is { } clipboard && ClipboardContent is { } clipboardContent)
await clipboard.SetTextAsync(clipboardContent.Text ?? String.Empty);
}
private async void PasteText(object? sender, RoutedEventArgs args)
{
if(Application.Current!.Clipboard is { } clipboard)
if (TopLevel.GetTopLevel(this)?.Clipboard is { } clipboard)
{
ClipboardContent.Text = await clipboard.GetTextAsync();
}
@@ -39,7 +46,7 @@ namespace ControlCatalog.Pages
private async void CopyTextDataObject(object? sender, RoutedEventArgs args)
{
if (Application.Current!.Clipboard is { } clipboard)
if (TopLevel.GetTopLevel(this)?.Clipboard is { } clipboard)
{
var dataObject = new DataObject();
dataObject.Set(DataFormats.Text, ClipboardContent.Text ?? string.Empty);
@@ -49,7 +56,7 @@ namespace ControlCatalog.Pages
private async void PasteTextDataObject(object? sender, RoutedEventArgs args)
{
if (Application.Current!.Clipboard is { } clipboard)
if (TopLevel.GetTopLevel(this)?.Clipboard is { } clipboard)
{
ClipboardContent.Text = await clipboard.GetDataAsync(DataFormats.Text) as string ?? string.Empty;
}
@@ -57,32 +64,63 @@ namespace ControlCatalog.Pages
private async void CopyFilesDataObject(object? sender, RoutedEventArgs args)
{
if (Application.Current!.Clipboard is { } clipboard)
if (TopLevel.GetTopLevel(this)?.Clipboard is { } clipboard)
{
var files = (ClipboardContent.Text ?? String.Empty)
.Split(new[] { Environment.NewLine }, StringSplitOptions.RemoveEmptyEntries);
if (files.Length == 0)
var storageProvider = TopLevel.GetTopLevel(this)!.StorageProvider;
var filesPath = (ClipboardContent.Text ?? string.Empty)
.Split(new[] { Environment.NewLine }, StringSplitOptions.RemoveEmptyEntries);
if (filesPath.Length == 0)
{
return;
}
var dataObject = new DataObject();
dataObject.Set(DataFormats.FileNames, files);
await clipboard.SetDataObjectAsync(dataObject);
List<string> invalidFile = new(filesPath.Length);
List<IStorageFile> files = new(filesPath.Length);
for (int i = 0; i < filesPath.Length; i++)
{
var file = await storageProvider.TryGetFileFromPathAsync(filesPath[i]);
if (file is null)
{
invalidFile.Add(filesPath[i]);
}
else
{
files.Add(file);
}
}
if (invalidFile.Count > 0)
{
NotificationManager.Show(new Notification("Warning", "There is one o more invalid path.", NotificationType.Warning));
}
if (files.Count > 0)
{
var dataObject = new DataObject();
dataObject.Set(DataFormats.Files, files);
await clipboard.SetDataObjectAsync(dataObject);
NotificationManager.Show(new Notification("Success", "Copy completated.", NotificationType.Success));
}
else
{
NotificationManager.Show(new Notification("Warning", "Any files to copy in Clipboard.", NotificationType.Warning));
}
}
}
private async void PasteFilesDataObject(object? sender, RoutedEventArgs args)
{
if (Application.Current!.Clipboard is { } clipboard)
if (TopLevel.GetTopLevel(this)?.Clipboard is { } clipboard)
{
var fiels = await clipboard.GetDataAsync(DataFormats.FileNames) as IEnumerable<string>;
ClipboardContent.Text = fiels != null ? string.Join(Environment.NewLine, fiels) : string.Empty;
var files = await clipboard.GetDataAsync(DataFormats.Files) as IEnumerable<Avalonia.Platform.Storage.IStorageItem>;
ClipboardContent.Text = files != null ? string.Join(Environment.NewLine, files.Select(f => f.TryGetLocalPath() ?? f.Name)) : string.Empty;
}
}
private async void GetFormats(object sender, RoutedEventArgs args)
{
if (Application.Current!.Clipboard is { } clipboard)
if (TopLevel.GetTopLevel(this)?.Clipboard is { } clipboard)
{
var formats = await clipboard.GetFormatsAsync();
ClipboardContent.Text = string.Join(Environment.NewLine, formats);
@@ -91,11 +129,11 @@ namespace ControlCatalog.Pages
private async void Clear(object sender, RoutedEventArgs args)
{
if (Application.Current!.Clipboard is { } clipboard)
if (TopLevel.GetTopLevel(this)?.Clipboard is { } clipboard)
{
await clipboard.ClearAsync();
}
}
}
}
@@ -28,6 +28,41 @@
<Grid Grid.Column="2"
Grid.Row="0"
RowDefinitions="Auto,Auto,Auto,Auto,Auto,Auto,Auto,Auto,Auto">
<Grid.Resources>
<x:Double x:Key="ColorSliderSize">24</x:Double>
<x:Double x:Key="ColorSliderTrackSize">18</x:Double>
<CornerRadius x:Key="ColorSliderCornerRadius">12</CornerRadius>
<CornerRadius x:Key="ColorSliderTrackCornerRadius">9</CornerRadius>
<!-- Due to 'SystemControlForegroundBaseHighBrush' usage this only works in Fluent theme. -->
<!-- Otherwise it would be necessary to make custom light/dark resources. -->
<ControlTheme x:Key="ColorSliderThumbTheme"
TargetType="Thumb">
<Setter Property="Background" Value="Transparent" />
<Setter Property="BorderBrush" Value="{DynamicResource SystemControlForegroundBaseHighBrush}" />
<Setter Property="BorderThickness" Value="5" />
<Setter Property="CornerRadius" Value="{DynamicResource ColorSliderCornerRadius}" />
<Setter Property="Template">
<Setter.Value>
<ControlTemplate>
<Grid>
<Border Background="{TemplateBinding Background}"
BorderBrush="{TemplateBinding BorderBrush}"
BorderThickness="{TemplateBinding BorderThickness}"
CornerRadius="{TemplateBinding CornerRadius}" />
<Ellipse Height="{TemplateBinding Height}"
Width="{TemplateBinding Width}"
Fill="Transparent"
Stroke="{TemplateBinding Foreground}"
StrokeThickness="1" />
</Grid>
</ControlTemplate>
</Setter.Value>
</Setter>
</ControlTheme>
</Grid.Resources>
<ColorSpectrum x:Name="ColorSpectrum1"
Grid.Row="0"
Color="Red"
@@ -18,7 +18,7 @@ namespace ControlCatalog.Pages
{
AvaloniaXamlLoader.Load(this);
var fontComboBox = this.Get<ComboBox>("fontComboBox");
fontComboBox.Items = FontManager.Current.SystemFonts;
fontComboBox.ItemsSource = FontManager.Current.SystemFonts;
fontComboBox.SelectedIndex = 0;
}
}
@@ -32,7 +32,7 @@ public partial class CompositionPage : UserControl
protected override void OnAttachedToVisualTree(VisualTreeAttachmentEventArgs e)
{
base.OnAttachedToVisualTree(e);
this.Get<ItemsControl>("Items").Items = CreateColorItems();
this.Get<ItemsControl>("Items").ItemsSource = CreateColorItems();
}
@@ -61,7 +61,7 @@
<Border.Styles>
<Style Selector="MenuFlyoutPresenter MenuItem" x:DataType="viewModels:MenuItemViewModel">
<Setter Property="Header" Value="{Binding Header}"/>
<Setter Property="Items" Value="{Binding Items}"/>
<Setter Property="ItemsSource" Value="{Binding Items}"/>
<Setter Property="Command" Value="{Binding Command}"/>
<Setter Property="CommandParameter" Value="{Binding CommandParameter}"/>
</Style>
@@ -51,13 +51,13 @@
<Border.Styles>
<Style Selector="ContextMenu MenuItem" x:DataType="viewModels:MenuItemViewModel">
<Setter Property="Header" Value="{Binding Header}"/>
<Setter Property="Items" Value="{Binding Items}"/>
<Setter Property="ItemsSource" Value="{Binding Items}"/>
<Setter Property="Command" Value="{Binding Command}"/>
<Setter Property="CommandParameter" Value="{Binding CommandParameter}"/>
</Style>
</Border.Styles>
<Border.ContextMenu>
<ContextMenu Items="{Binding MenuItems}" />
<ContextMenu ItemsSource="{Binding MenuItems}" />
</Border.ContextMenu>
<TextBlock Text="Dynamically Generated"/>
</Border>
+1 -1
View File
@@ -8,7 +8,7 @@
<TextBlock Classes="h2">Defines a cursor (mouse pointer)</TextBlock>
</StackPanel>
<ListBox Grid.Row="1" Items="{Binding StandardCursors}" Margin="0 8 8 8">
<ListBox Grid.Row="1" ItemsSource="{Binding StandardCursors}" Margin="0 8 8 8">
<ListBox.Styles>
<Style Selector="ListBoxItem">
<Setter Property="Cursor" Value="{Binding Cursor}" x:DataType="viewModels:StandardCursorModel"/>
@@ -133,17 +133,17 @@ namespace ControlCatalog.Pages
// 0,0 refers to the top-left of the control now. It is not prime time to draw gui stuff because it'll be under the world
var translateModifier = context.PushPreTransform(Avalonia.Matrix.CreateTranslation(new Avalonia.Vector(halfWidth, halfHeight)));
var translateModifier = context.PushTransform(Avalonia.Matrix.CreateTranslation(new Avalonia.Vector(halfWidth, halfHeight)));
// now 0,0 refers to the ViewportCenter(X,Y).
var rotationMatrix = Avalonia.Matrix.CreateRotation(Rotation);
var rotationModifier = context.PushPreTransform(rotationMatrix);
var rotationModifier = context.PushTransform(rotationMatrix);
// everything is rotated but not scaled
var scaleModifier = context.PushPreTransform(Avalonia.Matrix.CreateScale(Scale, -Scale));
var scaleModifier = context.PushTransform(Avalonia.Matrix.CreateScale(Scale, -Scale));
var mapPositionModifier = context.PushPreTransform(Matrix.CreateTranslation(new Vector(-ViewportCenterX, -ViewportCenterY)));
var mapPositionModifier = context.PushTransform(Matrix.CreateTranslation(new Vector(-ViewportCenterX, -ViewportCenterY)));
// now everything is rotated and scaled, and at the right position, now we're drawing strictly in world coordinates
@@ -106,7 +106,7 @@ namespace ControlCatalog.Pages
Directory = initialDirectory,
InitialFileName = initialFileName
}.ShowAsync(GetWindow());
results.Items = result;
results.ItemsSource = result;
resultsVisible.IsVisible = result?.Any() == true;
};
this.Get<Button>("OpenMultipleFiles").Click += async delegate
@@ -118,7 +118,7 @@ namespace ControlCatalog.Pages
Directory = lastSelectedDirectory?.Path is {IsAbsoluteUri:true} path ? path.LocalPath : null,
AllowMultiple = true
}.ShowAsync(GetWindow());
results.Items = result;
results.ItemsSource = result;
resultsVisible.IsVisible = result?.Any() == true;
};
this.Get<Button>("SaveFile").Click += async delegate
@@ -132,7 +132,7 @@ namespace ControlCatalog.Pages
DefaultExtension = filters?.Any() == true ? "txt" : null,
InitialFileName = "test.txt"
}.ShowAsync(GetWindow());
results.Items = new[] { result };
results.ItemsSource = new[] { result };
resultsVisible.IsVisible = result != null;
};
this.Get<Button>("SelectFolder").Click += async delegate
@@ -149,7 +149,7 @@ namespace ControlCatalog.Pages
else
{
SetFolder(await GetStorageProvider().TryGetFolderFromPathAsync(result));
results.Items = new[] { result };
results.ItemsSource = new[] { result };
resultsVisible.IsVisible = true;
}
};
@@ -164,7 +164,7 @@ namespace ControlCatalog.Pages
{
AllowDirectorySelection = true
});
results.Items = result;
results.ItemsSource = result;
resultsVisible.IsVisible = result?.Any() == true;
};
this.Get<Button>("DecoratedWindow").Click += delegate
@@ -324,15 +324,15 @@ namespace ControlCatalog.Pages
mappedResults.Add("+> " + FullPathOrName(selectedItem));
if (selectedItem is IStorageFolder folder)
{
foreach (var innerItems in await folder.GetItemsAsync())
await foreach (var innerItem in folder.GetItemsAsync())
{
mappedResults.Add("++> " + FullPathOrName(innerItems));
mappedResults.Add("++> " + FullPathOrName(innerItem));
}
}
}
}
results.Items = mappedResults;
results.ItemsSource = mappedResults;
resultsVisible.IsVisible = mappedResults.Any();
}
}
@@ -104,8 +104,12 @@ namespace ControlCatalog.Pages
}
else if (item is IStorageFolder folder)
{
var items = await folder.GetItemsAsync();
contentStr += $"Folder {item.Name}: items {items.Count}{Environment.NewLine}{Environment.NewLine}";
var childrenCount = 0;
await foreach (var _ in folder.GetItemsAsync())
{
childrenCount++;
}
contentStr += $"Folder {item.Name}: items {childrenCount}{Environment.NewLine}{Environment.NewLine}";
}
}
@@ -30,7 +30,7 @@
<Button Command="{Binding RemoveItemCommand}">Remove</Button>
<Button Command="{Binding SelectRandomItemCommand}">Select Random Item</Button>
</StackPanel>
<ListBox Items="{Binding Items}"
<ListBox ItemsSource="{Binding Items}"
Selection="{Binding Selection}"
DisplayMemberBinding="{Binding (viewModels:ItemModel).ID, StringFormat='{}Item {0:N0}'}"
AutoScrollToSelectedItem="{Binding AutoScrollToSelectedItem}"
+3 -3
View File
@@ -45,11 +45,11 @@
<StackPanel>
<TextBlock Classes="h3" Margin="4 8">Dyanamically generated</TextBlock>
<Menu Items="{Binding MenuItems}">
<Menu ItemsSource="{Binding MenuItems}">
<Menu.Styles>
<Style Selector="MenuItem" x:DataType="viewModels:MenuItemViewModel">
<Setter Property="Header" Value="{Binding Header}"/>
<Setter Property="Items" Value="{Binding Items}"/>
<Setter Property="ItemsSource" Value="{Binding Items}"/>
<Setter Property="Command" Value="{Binding Command}"/>
<Setter Property="CommandParameter" Value="{Binding CommandParameter}"/>
</Style>
@@ -68,7 +68,7 @@
<Separator/>
<MenuItem Header="Execu_te Script..." />
<Separator/>
<MenuItem Header="_Recent" Items="{Binding RecentItems}">
<MenuItem Header="_Recent" ItemsSource="{Binding RecentItems}">
<MenuItem.Styles>
<Style Selector="MenuItem" x:DataType="viewModels:MenuItemViewModel">
<Setter Property="Header" Value="{Binding Header}"/>
@@ -33,10 +33,10 @@ namespace ControlCatalog.Pages
{
new ContextMenu()
{
Items = new List<MenuItem>
{
new MenuItem() { Header = "Test" }, new MenuItem() { Header = "Test" }
}
Items =
{
new MenuItem() { Header = "Test" }, new MenuItem() { Header = "Test" }
}
}.Open((Control)sender);
}
@@ -27,7 +27,7 @@
</Grid>
<Grid Grid.Row="0" Grid.Column="1" Margin="8" ColumnDefinitions="Auto, 120" RowDefinitions="Auto,Auto,Auto,Auto,Auto">
<TextBlock Grid.Row="0" Grid.Column="0" VerticalAlignment="Center" Margin="2">FormatString:</TextBlock>
<ComboBox Grid.Row="0" Grid.Column="1" Items="{Binding Formats}" SelectedItem="{Binding SelectedFormat}"
<ComboBox Grid.Row="0" Grid.Column="1" ItemsSource="{Binding Formats}" SelectedItem="{Binding SelectedFormat}"
VerticalAlignment="Center" Margin="2">
<ComboBox.ItemTemplate>
<DataTemplate>
@@ -41,11 +41,11 @@
</ComboBox>
<TextBlock Grid.Row="1" Grid.Column="0" VerticalAlignment="Center" Margin="2">ButtonSpinnerLocation:</TextBlock>
<ComboBox Grid.Row="1" Grid.Column="1" Items="{Binding SpinnerLocations}" SelectedItem="{Binding #upDown.ButtonSpinnerLocation}"
<ComboBox Grid.Row="1" Grid.Column="1" ItemsSource="{Binding SpinnerLocations}" SelectedItem="{Binding #upDown.ButtonSpinnerLocation}"
VerticalAlignment="Center" Margin="2"/>
<TextBlock Grid.Row="2" Grid.Column="0" VerticalAlignment="Center" Margin="2">CultureInfo:</TextBlock>
<ComboBox x:Name="CultureSelector" Grid.Row="2" Grid.Column="1" Items="{Binding Cultures}"
<ComboBox x:Name="CultureSelector" Grid.Row="2" Grid.Column="1" ItemsSource="{Binding Cultures}"
VerticalAlignment="Center" Margin="2"/>
<TextBlock Grid.Row="3" Grid.Column="0" VerticalAlignment="Center" Margin="2">Watermark:</TextBlock>
@@ -21,7 +21,7 @@
Margin="5">
<ListBox HorizontalAlignment="Stretch"
VerticalAlignment="Top"
Items="{Binding Items}"/>
ItemsSource="{Binding Items}"/>
</RefreshContainer>
</DockPanel>
</UserControl>
@@ -1,222 +0,0 @@
<UserControl xmlns="https://github.com/avaloniaui"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
d:DesignHeight="800"
d:DesignWidth="400"
x:Class="ControlCatalog.Pages.ScrollSnapPage"
xmlns:pages="using:ControlCatalog.Pages"
x:DataType="pages:ScrollSnapPageViewModel">
<StackPanel Orientation="Vertical" Spacing="4">
<TextBlock TextWrapping="Wrap"
Classes="h2">Scrollviewer can snap supported content both vertically and horizontally. Snapping occurs from scrolling with touch or pen.</TextBlock>
<Grid RowDefinitions="Auto, Auto, Auto, Auto, Auto">
<StackPanel Orientation="Horizontal"
Spacing="4">
<StackPanel Orientation="Vertical"
Spacing="4">
<TextBlock Text="Snap Point Type" />
<ComboBox Items="{Binding AvailableSnapPointsType}"
SelectedItem="{Binding SnapPointsType}" />
</StackPanel>
<StackPanel Orientation="Vertical"
Spacing="4">
<TextBlock Text="Snap Point Alignment" />
<ComboBox Items="{Binding AvailableSnapPointsAlignment}"
SelectedItem="{Binding SnapPointsAlignment}" />
</StackPanel>
<ToggleSwitch IsChecked="{Binding AreSnapPointsRegular}"
OffContent="No"
OnContent="Yes"
Content="Are Snap Points regular?" />
</StackPanel>
<TextBlock TextWrapping="Wrap"
Grid.Row="1"
Margin="0,10"
Classes="h2">Vertical Snapping</TextBlock>
<Border
BorderBrush="Green"
BorderThickness="1"
Padding="0"
Grid.Row="2"
Margin="10, 5">
<ScrollViewer x:Name="VerticalSnapsScrollViewer"
VerticalSnapPointsType="{Binding SnapPointsType}"
VerticalSnapPointsAlignment="{Binding SnapPointsAlignment}"
HorizontalAlignment="Stretch"
Height="350"
HorizontalScrollBarVisibility="Disabled">
<StackPanel AreVerticalSnapPointsRegular="{Binding AreSnapPointsRegular}"
Orientation="Vertical"
HorizontalAlignment="Stretch">
<Border Padding="5, 30"
BorderBrush="Red"
HorizontalAlignment="Stretch"
BorderThickness="1">
<TextBlock FontWeight="Bold"
Text="Child 1"/>
</Border>
<Border Padding="5, 30"
BorderBrush="Red"
HorizontalAlignment="Stretch"
BorderThickness="1">
<TextBlock FontWeight="Bold"
Text="Child 2"/>
</Border>
<Border Padding="5, 20"
BorderBrush="Red"
HorizontalAlignment="Stretch"
BorderThickness="1">
<TextBlock FontWeight="Bold"
Text="Child 3"/>
</Border>
<Border Padding="5, 30"
BorderBrush="Red"
HorizontalAlignment="Stretch"
BorderThickness="1">
<TextBlock FontWeight="Bold"
Text="Child 4"/>
</Border>
<Border Padding="5, 30"
BorderBrush="Red"
HorizontalAlignment="Stretch"
BorderThickness="1">
<TextBlock FontWeight="Bold"
Text="Child 5"/>
</Border>
<Border Padding="5, 30"
BorderBrush="Red"
HorizontalAlignment="Stretch"
BorderThickness="1">
<TextBlock FontWeight="Bold"
Text="Child 6"/>
</Border>
<Border Padding="5,8"
BorderBrush="Red"
HorizontalAlignment="Stretch"
BorderThickness="1">
<TextBlock FontWeight="Bold"
Text="Child 7"/>
</Border>
<Border Padding="5, 30"
BorderBrush="Red"
HorizontalAlignment="Stretch"
BorderThickness="1">
<TextBlock FontWeight="Bold"
Text="Child 8"/>
</Border>
<Border Padding="5,4"
BorderBrush="Red"
HorizontalAlignment="Stretch"
BorderThickness="1">
<TextBlock FontWeight="Bold"
Text="Child 9"/>
</Border>
<Border Padding="5, 30"
BorderBrush="Red"
HorizontalAlignment="Stretch"
BorderThickness="1">
<TextBlock FontWeight="Bold"
Text="Child 20"/>
</Border>
<Border Padding="5, 30"
BorderBrush="Red"
HorizontalAlignment="Stretch"
BorderThickness="1">
<TextBlock FontWeight="Bold"
Text="Child 11"/>
</Border>
<Border Padding="5, 30"
BorderBrush="Red"
HorizontalAlignment="Stretch"
BorderThickness="1">
<TextBlock FontWeight="Bold"
Text="Child 12"/>
</Border>
</StackPanel>
</ScrollViewer>
</Border>
<TextBlock TextWrapping="Wrap"
Grid.Row="3"
Margin="0,10"
Classes="h2">Horizontal Snapping</TextBlock>
<Border
BorderBrush="Green"
BorderThickness="1"
Padding="0"
Grid.Row="4"
Margin="10, 10">
<ScrollViewer x:Name="HorizontalSnapsScrollViewer"
HorizontalSnapPointsType="{Binding SnapPointsType}"
HorizontalSnapPointsAlignment="{Binding SnapPointsAlignment}"
HorizontalAlignment="Stretch"
Height="350"
HorizontalScrollBarVisibility="Auto"
VerticalScrollBarVisibility="Disabled">
<StackPanel AreHorizontalSnapPointsRegular="{Binding AreSnapPointsRegular}"
Orientation="Horizontal"
HorizontalAlignment="Stretch">
<Border Padding="5, 30"
Width="300"
BorderBrush="Red"
HorizontalAlignment="Stretch"
BorderThickness="1">
<TextBlock FontWeight="Bold"
VerticalAlignment="Center"
Text="Child 1"/>
</Border>
<Border Padding="5, 30"
Width="300"
BorderBrush="Red"
VerticalAlignment="Stretch"
BorderThickness="1">
<TextBlock FontWeight="Bold"
VerticalAlignment="Center"
Text="Child 2"/>
</Border>
<Border Padding="5, 20"
Width="300"
BorderBrush="Red"
VerticalAlignment="Stretch"
BorderThickness="1">
<TextBlock FontWeight="Bold"
VerticalAlignment="Center"
Text="Child 3"/>
</Border>
<Border Padding="5, 30"
Width="300"
BorderBrush="Red"
VerticalAlignment="Stretch"
BorderThickness="1">
<TextBlock FontWeight="Bold"
VerticalAlignment="Center"
Text="Child 4"/>
</Border>
<Border Padding="5, 30"
Width="300"
BorderBrush="Red"
VerticalAlignment="Stretch"
BorderThickness="1">
<TextBlock FontWeight="Bold"
VerticalAlignment="Center"
Text="Child 5"/>
</Border>
<Border Padding="5, 30"
Width="300"
BorderBrush="Red"
VerticalAlignment="Stretch"
BorderThickness="1">
<TextBlock FontWeight="Bold"
VerticalAlignment="Center"
Text="Child 6"/>
</Border>
</StackPanel>
</ScrollViewer>
</Border>
</Grid>
</StackPanel>
</UserControl>
@@ -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>()
{
SnapPointsType.None,
SnapPointsType.Mandatory,
SnapPointsType.MandatorySingle
};
AvailableSnapPointsAlignment = new List<SnapPointsAlignment>()
{
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<SnapPointsType> AvailableSnapPointsType { get; }
public List<SnapPointsAlignment> AvailableSnapPointsAlignment { get; }
}
public class ScrollSnapPage : UserControl
{
public ScrollSnapPage()
{
this.InitializeComponent();
DataContext = new ScrollSnapPageViewModel();
}
private void InitializeComponent()
{
AvaloniaXamlLoader.Load(this);
}
}
}
@@ -3,35 +3,267 @@
xmlns:pages="using:ControlCatalog.Pages"
x:Class="ControlCatalog.Pages.ScrollViewerPage"
x:DataType="pages:ScrollViewerPageViewModel">
<StackPanel Orientation="Vertical" Spacing="20">
<TextBlock TextWrapping="Wrap" Classes="h2">Allows for horizontal and vertical content scrolling. Supports snapping on touch and pointer wheel scrolling.</TextBlock>
<TabControl>
<TabItem Header="ScrollViewer">
<StackPanel Orientation="Vertical"
Spacing="20">
<TextBlock TextWrapping="Wrap"
Classes="h2">Allows for horizontal and vertical content scrolling. Supports snapping on touch and pointer wheel scrolling.</TextBlock>
<Grid ColumnDefinitions="Auto, *">
<StackPanel Orientation="Vertical" Spacing="4">
<ToggleSwitch IsChecked="{Binding AllowAutoHide}" Content="Allow auto hide" />
<ToggleSwitch IsChecked="{Binding EnableInertia}" Content="Enable Inertia" />
<Grid ColumnDefinitions="Auto, *">
<StackPanel Orientation="Vertical"
Spacing="4">
<ToggleSwitch IsChecked="{Binding AllowAutoHide}"
Content="Allow auto hide" />
<ToggleSwitch IsChecked="{Binding EnableInertia}"
Content="Enable Inertia" />
<StackPanel Orientation="Vertical" Spacing="4">
<TextBlock Text="Horizontal Scroll" />
<ComboBox Items="{Binding AvailableVisibility}" SelectedItem="{Binding HorizontalScrollVisibility}" />
</StackPanel>
<StackPanel Orientation="Vertical"
Spacing="4">
<TextBlock Text="Horizontal Scroll" />
<ComboBox ItemsSource="{Binding AvailableVisibility}"
SelectedItem="{Binding HorizontalScrollVisibility}" />
</StackPanel>
<StackPanel Orientation="Vertical" Spacing="4">
<TextBlock Text="Vertical Scroll" />
<ComboBox Items="{Binding AvailableVisibility}" SelectedItem="{Binding VerticalScrollVisibility}" />
</StackPanel>
<StackPanel Orientation="Vertical"
Spacing="4">
<TextBlock Text="Vertical Scroll" />
<ComboBox ItemsSource="{Binding AvailableVisibility}"
SelectedItem="{Binding VerticalScrollVisibility}" />
</StackPanel>
</StackPanel>
<ScrollViewer x:Name="ScrollViewer"
Grid.Column="1"
Width="400"
Height="400"
IsScrollInertiaEnabled="{Binding EnableInertia}"
AllowAutoHide="{Binding AllowAutoHide}"
HorizontalScrollBarVisibility="{Binding HorizontalScrollVisibility}"
VerticalScrollBarVisibility="{Binding VerticalScrollVisibility}">
<Image Width="800"
Height="800"
Stretch="UniformToFill"
Source="/Assets/delicate-arch-896885_640.jpg" />
</ScrollViewer>
</Grid>
</StackPanel>
</TabItem>
<TabItem Header="Snapping">
<StackPanel Orientation="Vertical"
Spacing="4">
<TextBlock TextWrapping="Wrap"
Classes="h2">Scrollviewer can snap supported content both vertically and horizontally. Snapping occurs from scrolling with touch or pen.</TextBlock>
<ScrollViewer x:Name="ScrollViewer"
Grid.Column="1"
Width="400" Height="400"
IsScrollInertiaEnabled="{Binding EnableInertia}"
AllowAutoHide="{Binding AllowAutoHide}"
HorizontalScrollBarVisibility="{Binding HorizontalScrollVisibility}"
VerticalScrollBarVisibility="{Binding VerticalScrollVisibility}">
<Image Width="800" Height="800" Stretch="UniformToFill"
Source="/Assets/delicate-arch-896885_640.jpg" />
</ScrollViewer>
</Grid>
</StackPanel>
<Grid RowDefinitions="Auto, Auto, Auto, Auto, Auto">
<StackPanel Orientation="Horizontal"
Spacing="4">
<StackPanel Orientation="Vertical"
Spacing="4">
<TextBlock Text="Snap Point Type" />
<ComboBox ItemsSource="{Binding AvailableSnapPointsType}"
SelectedItem="{Binding SnapPointsType}" />
</StackPanel>
<StackPanel Orientation="Vertical"
Spacing="4">
<TextBlock Text="Snap Point Alignment" />
<ComboBox ItemsSource="{Binding AvailableSnapPointsAlignment}"
SelectedItem="{Binding SnapPointsAlignment}" />
</StackPanel>
<ToggleSwitch IsChecked="{Binding AreSnapPointsRegular}"
OffContent="No"
OnContent="Yes"
Content="Are Snap Points regular?" />
</StackPanel>
<TextBlock TextWrapping="Wrap"
Grid.Row="1"
Margin="0,10"
Classes="h2">Vertical Snapping</TextBlock>
<Border
BorderBrush="Green"
BorderThickness="1"
Padding="0"
Grid.Row="2"
Margin="10, 5">
<ScrollViewer x:Name="VerticalSnapsScrollViewer"
VerticalSnapPointsType="{Binding SnapPointsType}"
VerticalSnapPointsAlignment="{Binding SnapPointsAlignment}"
HorizontalAlignment="Stretch"
Height="350"
HorizontalScrollBarVisibility="Disabled">
<StackPanel AreVerticalSnapPointsRegular="{Binding AreSnapPointsRegular}"
Orientation="Vertical"
HorizontalAlignment="Stretch">
<Border Padding="5, 30"
BorderBrush="Red"
HorizontalAlignment="Stretch"
BorderThickness="1">
<TextBlock FontWeight="Bold"
Text="Child 1"/>
</Border>
<Border Padding="5, 30"
BorderBrush="Red"
HorizontalAlignment="Stretch"
BorderThickness="1">
<TextBlock FontWeight="Bold"
Text="Child 2"/>
</Border>
<Border Padding="5, 20"
BorderBrush="Red"
HorizontalAlignment="Stretch"
BorderThickness="1">
<TextBlock FontWeight="Bold"
Text="Child 3"/>
</Border>
<Border Padding="5, 30"
BorderBrush="Red"
HorizontalAlignment="Stretch"
BorderThickness="1">
<TextBlock FontWeight="Bold"
Text="Child 4"/>
</Border>
<Border Padding="5, 30"
BorderBrush="Red"
HorizontalAlignment="Stretch"
BorderThickness="1">
<TextBlock FontWeight="Bold"
Text="Child 5"/>
</Border>
<Border Padding="5, 30"
BorderBrush="Red"
HorizontalAlignment="Stretch"
BorderThickness="1">
<TextBlock FontWeight="Bold"
Text="Child 6"/>
</Border>
<Border Padding="5,8"
BorderBrush="Red"
HorizontalAlignment="Stretch"
BorderThickness="1">
<TextBlock FontWeight="Bold"
Text="Child 7"/>
</Border>
<Border Padding="5, 30"
BorderBrush="Red"
HorizontalAlignment="Stretch"
BorderThickness="1">
<TextBlock FontWeight="Bold"
Text="Child 8"/>
</Border>
<Border Padding="5,4"
BorderBrush="Red"
HorizontalAlignment="Stretch"
BorderThickness="1">
<TextBlock FontWeight="Bold"
Text="Child 9"/>
</Border>
<Border Padding="5, 30"
BorderBrush="Red"
HorizontalAlignment="Stretch"
BorderThickness="1">
<TextBlock FontWeight="Bold"
Text="Child 20"/>
</Border>
<Border Padding="5, 30"
BorderBrush="Red"
HorizontalAlignment="Stretch"
BorderThickness="1">
<TextBlock FontWeight="Bold"
Text="Child 11"/>
</Border>
<Border Padding="5, 30"
BorderBrush="Red"
HorizontalAlignment="Stretch"
BorderThickness="1">
<TextBlock FontWeight="Bold"
Text="Child 12"/>
</Border>
</StackPanel>
</ScrollViewer>
</Border>
<TextBlock TextWrapping="Wrap"
Grid.Row="3"
Margin="0,10"
Classes="h2">Horizontal Snapping</TextBlock>
<Border
BorderBrush="Green"
BorderThickness="1"
Padding="0"
Grid.Row="4"
Margin="10, 10">
<ScrollViewer x:Name="HorizontalSnapsScrollViewer"
HorizontalSnapPointsType="{Binding SnapPointsType}"
HorizontalSnapPointsAlignment="{Binding SnapPointsAlignment}"
HorizontalAlignment="Stretch"
Height="350"
HorizontalScrollBarVisibility="Auto"
VerticalScrollBarVisibility="Disabled">
<StackPanel AreHorizontalSnapPointsRegular="{Binding AreSnapPointsRegular}"
Orientation="Horizontal"
HorizontalAlignment="Stretch">
<Border Padding="5, 30"
Width="300"
BorderBrush="Red"
HorizontalAlignment="Stretch"
BorderThickness="1">
<TextBlock FontWeight="Bold"
VerticalAlignment="Center"
Text="Child 1"/>
</Border>
<Border Padding="5, 30"
Width="300"
BorderBrush="Red"
VerticalAlignment="Stretch"
BorderThickness="1">
<TextBlock FontWeight="Bold"
VerticalAlignment="Center"
Text="Child 2"/>
</Border>
<Border Padding="5, 20"
Width="300"
BorderBrush="Red"
VerticalAlignment="Stretch"
BorderThickness="1">
<TextBlock FontWeight="Bold"
VerticalAlignment="Center"
Text="Child 3"/>
</Border>
<Border Padding="5, 30"
Width="300"
BorderBrush="Red"
VerticalAlignment="Stretch"
BorderThickness="1">
<TextBlock FontWeight="Bold"
VerticalAlignment="Center"
Text="Child 4"/>
</Border>
<Border Padding="5, 30"
Width="300"
BorderBrush="Red"
VerticalAlignment="Stretch"
BorderThickness="1">
<TextBlock FontWeight="Bold"
VerticalAlignment="Center"
Text="Child 5"/>
</Border>
<Border Padding="5, 30"
Width="300"
BorderBrush="Red"
VerticalAlignment="Stretch"
BorderThickness="1">
<TextBlock FontWeight="Bold"
VerticalAlignment="Center"
Text="Child 6"/>
</Border>
</StackPanel>
</ScrollViewer>
</Border>
</Grid>
</StackPanel>
</TabItem>
</TabControl>
</UserControl>
@@ -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>()
{
SnapPointsType.None,
SnapPointsType.Mandatory,
SnapPointsType.MandatorySingle
};
AvailableSnapPointsAlignment = new List<SnapPointsAlignment>()
{
SnapPointsAlignment.Near,
SnapPointsAlignment.Center,
SnapPointsAlignment.Far,
};
HorizontalScrollVisibility = ScrollBarVisibility.Auto;
VerticalScrollVisibility = ScrollBarVisibility.Auto;
AllowAutoHide = true;
@@ -54,6 +71,26 @@ namespace ControlCatalog.Pages
}
public List<ScrollBarVisibility> 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<SnapPointsType> AvailableSnapPointsType { get; }
public List<SnapPointsAlignment> AvailableSnapPointsAlignment { get; }
}
public class ScrollViewerPage : UserControl
@@ -51,7 +51,7 @@
Text="From DataTemplate">
</TextBlock>
<TabControl
Items="{Binding Tabs}"
ItemsSource="{Binding Tabs}"
Margin="0 16"
DisplayMemberBinding="{Binding Header, x:DataType=viewModels:TabControlPageViewModelItem}"
TabStripPlacement="{Binding TabPlacement}">
@@ -18,7 +18,7 @@
<Separator Margin="0 16"/>
<TextBlock Classes="h1">Dynamically generated</TextBlock>
<TabStrip Items="{Binding Tabs}">
<TabStrip ItemsSource="{Binding Tabs}">
<TabStrip.Styles>
<Style Selector="TabStripItem" x:DataType="viewModels:TabControlPageViewModelItem">
<Setter Property="IsEnabled" Value="{Binding IsEnabled}"/>
@@ -12,7 +12,7 @@ namespace ControlCatalog.Pages
{
InitializeComponent();
Selector.Items = new[]
Selector.ItemsSource = new[]
{
ThemeVariant.Default,
ThemeVariant.Dark,
@@ -53,7 +53,7 @@
<StackPanel Margin="5" Spacing="5" Grid.IsSharedSizeScope="True">
<HeaderedContentControl Header="Select a transition">
<ComboBox Items="{Binding PageTransitions}" SelectedItem="{Binding SelectedTransition}" />
<ComboBox ItemsSource="{Binding PageTransitions}" SelectedItem="{Binding SelectedTransition}" />
</HeaderedContentControl>
<HeaderedContentControl Header="Duration">
<NumericUpDown Value="{Binding Duration}" Increment="250" Minimum="100" />
@@ -11,7 +11,7 @@
HorizontalAlignment="Center"
Spacing="16">
<StackPanel Orientation="Vertical" Spacing="8">
<TreeView Items="{Binding Items}" SelectedItems="{Binding SelectedItems}" SelectionMode="{Binding SelectionMode}" Width="250" Height="350">
<TreeView ItemsSource="{Binding Items}" SelectedItems="{Binding SelectedItems}" SelectionMode="{Binding SelectionMode}" Width="250" Height="350">
<TreeView.ItemTemplate>
<TreeDataTemplate ItemsSource="{Binding Children}">
<TextBlock Text="{Binding Header}"/>
+10 -1
View File
@@ -109,7 +109,7 @@
<StackPanel DockPanel.Dock="Bottom">
<Button Name="ListBoxSelectionClear">Clear Selection</Button>
</StackPanel>
<ListBox Name="BasicListBox" Items="{Binding ListBoxItems}" SelectionMode="Multiple"/>
<ListBox Name="BasicListBox" ItemsSource="{Binding ListBoxItems}" SelectionMode="Multiple"/>
</DockPanel>
</TabItem>
@@ -151,6 +151,12 @@
<ComboBoxItem Name="ShowWindowStateMaximized">Maximized</ComboBoxItem>
<ComboBoxItem Name="ShowWindowStateFullScreen">FullScreen</ComboBoxItem>
</ComboBox>
<ComboBox Name="ShowWindowSystemDecorations" SelectedIndex="2">
<ComboBoxItem Name="ShowWindowSystemDecorationsNone">None</ComboBoxItem>
<ComboBoxItem Name="ShowWindowSystemDecorationsBorderOnly">BorderOnly</ComboBoxItem>
<ComboBoxItem Name="ShowWindowSystemDecorationsFull">Full</ComboBoxItem>
</ComboBox>
<CheckBox Name="ShowWindowExtendClientAreaToDecorationsHint">ExtendClientAreaToDecorationsHint</CheckBox>
<CheckBox Name="ShowWindowCanResize" IsChecked="True">Can Resize</CheckBox>
<Button Name="ShowWindow">Show Window</Button>
<Button Name="SendToBack">Send to Back</Button>
@@ -167,6 +173,9 @@
<TabItem Header="SliderTab">
<Slider VerticalAlignment="Top" Name="Slider" Value="30"/>
</TabItem>
<TabItem Header="ScrollBarTab">
<ScrollBar Name="MyScrollBar" Orientation="Horizontal" AllowAutoHide="False" Width="200" Height="30" Value="20"/>
</TabItem>
</TabControl>
</DockPanel>
</Window>
@@ -68,6 +68,8 @@ namespace IntegrationTestApp
var locationComboBox = this.GetControl<ComboBox>("ShowWindowLocation");
var stateComboBox = this.GetControl<ComboBox>("ShowWindowState");
var size = !string.IsNullOrWhiteSpace(sizeTextBox.Text) ? Size.Parse(sizeTextBox.Text) : (Size?)null;
var systemDecorations = this.GetControl<ComboBox>("ShowWindowSystemDecorations");
var extendClientArea = this.GetControl<CheckBox>("ShowWindowExtendClientAreaToDecorationsHint");
var canResizeCheckBox = this.GetControl<CheckBox>("ShowWindowCanResize");
var owner = (Window)this.GetVisualRoot()!;
@@ -95,6 +97,8 @@ namespace IntegrationTestApp
}
sizeTextBox.Text = string.Empty;
window.ExtendClientAreaToDecorationsHint = extendClientArea.IsChecked ?? false;
window.SystemDecorations = (SystemDecorations)systemDecorations.SelectedIndex;
window.WindowState = (WindowState)stateComboBox.SelectedIndex;
switch (modeComboBox.SelectedIndex)
@@ -158,7 +162,7 @@ namespace IntegrationTestApp
var popup = new Popup
{
WindowManagerAddShadowHint = false,
PlacementMode = PlacementMode.AnchorAndGravity,
Placement = PlacementMode.AnchorAndGravity,
PlacementAnchor = PopupAnchor.Top,
PlacementGravity = PopupGravity.Bottom,
Width= 200,
@@ -6,7 +6,7 @@
x:DataType="Window"
Title="Show Window Test">
<integrationTestApp:MeasureBorder Name="MyBorder">
<Grid ColumnDefinitions="Auto,Auto" RowDefinitions="Auto,Auto,Auto,Auto,Auto,Auto,Auto,Auto,Auto,Auto,Auto">
<Grid ColumnDefinitions="Auto,Auto" RowDefinitions="Auto,Auto,Auto,Auto,Auto,Auto,Auto,Auto,Auto,Auto,Auto,Auto,Auto">
<Label Grid.Column="0" Grid.Row="1">Client Size</Label>
<TextBox Name="CurrentClientSize" Grid.Column="1" Grid.Row="1" IsReadOnly="True"
Text="{Binding ClientSize, Mode=OneWay}" />
@@ -35,13 +35,25 @@
<ComboBoxItem Name="WindowStateFullScreen">FullScreen</ComboBoxItem>
</ComboBox>
<Label Grid.Column="0" Grid.Row="8">Order (mac)</Label>
<TextBox Name="CurrentOrder" Grid.Column="1" Grid.Row="8" IsReadOnly="True" />
<Label Grid.Row="9" Content="MeasuredWith:" />
<TextBlock Grid.Column="1" Grid.Row="9" Name="CurrentMeasuredWithText" Text="{Binding #MyBorder.MeasuredWith}" />
<Label Grid.Column="0" Grid.Row="8">SystemDecorations</Label>
<ComboBox Name="CurrentSystemDecorations" Grid.Column="1" Grid.Row="8" SelectedIndex="{Binding SystemDecorations}">
<ComboBoxItem Name="SystemDecorationsNone">None</ComboBoxItem>
<ComboBoxItem Name="SystemDecorationsBorderOnly">BorderOnly</ComboBoxItem>
<ComboBoxItem Name="SystemDecorationsFull">Full</ComboBoxItem>
</ComboBox>
<Button Name="HideButton" Grid.Row="10" Command="{Binding $parent[Window].Hide}">Hide</Button>
<CheckBox Name="CurrentExtendClientAreaToDecorationsHint" Grid.ColumnSpan="2" Grid.Row="9"
IsChecked="{Binding ExtendClientAreaToDecorationsHint}">
ExtendClientAreaToDecorationsHint
</CheckBox>
<Label Grid.Column="0" Grid.Row="10">Order (mac)</Label>
<TextBox Name="CurrentOrder" Grid.Column="1" Grid.Row="10" IsReadOnly="True" />
<Label Grid.Row="11" Content="MeasuredWith:" />
<TextBlock Grid.Column="1" Grid.Row="11" Name="CurrentMeasuredWithText" Text="{Binding #MyBorder.MeasuredWith}" />
<Button Name="HideButton" Grid.Row="12" Command="{Binding $parent[Window].Hide}">Hide</Button>
</Grid>
</integrationTestApp:MeasureBorder>
@@ -308,6 +308,41 @@
</Animation>
</Style.Animations>
</Style>
<Style Selector="Border.Blur">
<Style.Animations>
<Animation Duration="0:0:3"
IterationCount="Infinite"
PlaybackDirection="Alternate">
<KeyFrame Cue="0%">
<Setter Property="Effect" Value="blur(0)"/>
</KeyFrame>
<KeyFrame Cue="100%">
<Setter Property="Effect" Value="blur(10)"/>
</KeyFrame>
</Animation>
</Style.Animations>
<Setter Property="Child" Value="{StaticResource Acorn}"/>
</Style>
<Style Selector="Border.DropShadow">
<Style.Animations>
<Animation Duration="0:0:3"
IterationCount="Infinite"
PlaybackDirection="Alternate">
<KeyFrame Cue="0%">
<Setter Property="Effect" Value="drop-shadow(0 0 0)"/>
</KeyFrame>
<KeyFrame Cue="35%">
<Setter Property="Effect" Value="drop-shadow(5 5 0 Green)"/>
</KeyFrame>
<KeyFrame Cue="70%">
<Setter Property="Effect" Value="drop-shadow(5 5 5 Red)"/>
</KeyFrame>
<KeyFrame Cue="100%">
<Setter Property="Effect" Value="drop-shadow(20 -5 5 Blue)"/>
</KeyFrame>
</Animation>
</Style.Animations>
</Style>
</Styles>
</UserControl.Styles>
<Grid>
@@ -332,6 +367,11 @@
<Border Classes="Test Rect8" Child="{x:Null}" />
<Border Classes="Test Rect9" Child="{x:Null}" />
<Border Classes="Test Rect10" Child="{x:Null}" />
<Border Classes="Test Blur" Background="#ffa0a0a0" BorderThickness="4" BorderBrush="Yellow" Padding="10"/>
<Border Classes="Test DropShadow" Background="Transparent" BorderThickness="4" BorderBrush="Yellow">
<TextBlock VerticalAlignment="Center" HorizontalAlignment="Center">Drop
Shadow</TextBlock>
</Border>
</WrapPanel>
</StackPanel>
</Grid>
@@ -29,7 +29,7 @@ namespace RenderDemo.Pages
public override void Render(DrawingContext context)
{
using (var ctx = _bitmap.CreateDrawingContext())
using (ctx.PushPostTransform(Matrix.CreateTranslation(-100, -100)
using (ctx.PushTransform(Matrix.CreateTranslation(-100, -100)
* Matrix.CreateRotation(_st.Elapsed.TotalSeconds)
* Matrix.CreateTranslation(100, 100)))
{
@@ -57,7 +57,7 @@ namespace ControlSamples
{
if (_splitView is not null && _splitView.DisplayMode == SplitViewDisplayMode.Overlay)
{
_splitView.SetValue(SplitView.IsPaneOpenProperty, false, Avalonia.Data.BindingPriority.Animation);
_splitView.SetCurrentValue(SplitView.IsPaneOpenProperty, false);
}
}
}
+14
View File
@@ -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>
+20
View File
@@ -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();
}
}
-7
View File
@@ -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>
-21
View File
@@ -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();
}
}
}
+190
View File
@@ -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 Items="{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 Items="{Binding ScrollBarVisibilities}"
SelectedItem="{Binding HorizontalScrollBarVisibility}"/>
<TextBlock>Vert. ScrollBar</TextBlock>
<ComboBox Items="{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"
Items="{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);
}
}
}
+23
View File
@@ -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);
+10 -11
View File
@@ -1,15 +1,14 @@
using Avalonia;
namespace VirtualizationDemo
{
class Program
{
public static AppBuilder BuildAvaloniaApp()
=> AppBuilder.Configure<App>()
.UsePlatformDetect()
.LogToTrace();
namespace VirtualizationDemo;
public static int Main(string[] args)
=> BuildAvaloniaApp().StartWithClassicDesktopLifetime(args);
}
class Program
{
public static AppBuilder BuildAvaloniaApp()
=> AppBuilder.Configure<App>()
.UsePlatformDetect()
.LogToTrace();
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
namespace VirtualizationDemo.ViewModels;
internal class MainWindowViewModel : ViewModelBase
{
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); }
}
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;
}
}
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">
&#x27F3;
</Button>
<Button DockPanel.Dock="Right"
Command="{Binding ExecuteScrollToIndex}"
ToolTip.Tip="Execute">
&#11152;
</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">&#x2B;</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>
@@ -1,14 +1,11 @@
using System;
using Android.Content;
using Android.Runtime;
using Android.Text;
using Android.Views;
using Android.Views.InputMethods;
using Avalonia.Android.Platform.SkiaPlatform;
using Avalonia.Controls.Presenters;
using Avalonia.Input;
using Avalonia.Input.TextInput;
using Avalonia.Reactive;
namespace Avalonia.Android
{
@@ -99,6 +96,9 @@ namespace Avalonia.Android
{
_host.InitEditorInfo((topLevel, outAttrs) =>
{
if (_client == null)
return null;
_inputConnection = new AvaloniaInputConnection(topLevel, this);
outAttrs.InputType = options.ContentType switch
@@ -38,7 +38,6 @@ namespace Avalonia.Android
Options = AvaloniaLocator.Current.GetService<AndroidPlatformOptions>() ?? new AndroidPlatformOptions();
AvaloniaLocator.CurrentMutable
.Bind<IClipboard>().ToTransient<ClipboardImpl>()
.Bind<ICursorFactory>().ToTransient<CursorFactory>()
.Bind<IWindowingPlatform>().ToConstant(new WindowingPlatformStub())
.Bind<IKeyboardDevice>().ToSingleton<AndroidKeyboardDevice>()
@@ -21,8 +21,6 @@ namespace Avalonia.Android
_handler = new Handler(App.Context.MainLooper);
}
public void RunLoop(CancellationToken cancellationToken) => throw new NotSupportedException();
public IDisposable StartTimer(DispatcherPriority priority, TimeSpan interval, Action tick)
{
if (interval.TotalMilliseconds < 10)
@@ -10,6 +10,7 @@
<ItemGroup>
<ProjectReference Include="..\..\..\packages\Avalonia\Avalonia.csproj" />
<PackageReference Include="Xamarin.AndroidX.AppCompat" Version="1.3.1.3" />
<PackageReference Include="Xamarin.AndroidX.DocumentFile" Version="1.0.1.16" />
<PackageReference Include="Xamarin.AndroidX.Lifecycle.ViewModel" Version="2.3.1.3" />
</ItemGroup>
<ItemGroup>
@@ -32,10 +32,6 @@ namespace Avalonia.Android
{
lifetime.View = View;
}
Window?.ClearFlags(WindowManagerFlags.TranslucentStatus);
Window?.AddFlags(WindowManagerFlags.DrawsSystemBarBackgrounds);
base.OnCreate(savedInstanceState);
SetContentView(View);
@@ -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<SafeAreaChangedArgs> 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
@@ -2,32 +2,26 @@ using System;
using System.Threading.Tasks;
using Android.Content;
using Android.Runtime;
using Android.Views;
using Avalonia.Input;
using Avalonia.Input.Platform;
using Avalonia.Platform;
namespace Avalonia.Android.Platform
{
internal class ClipboardImpl : IClipboard
{
private Context context = (AvaloniaLocator.Current.GetService<IWindowImpl>() as View).Context;
private ClipboardManager? _clipboardManager;
private ClipboardManager ClipboardManager
internal ClipboardImpl(ClipboardManager? value)
{
get
{
return this.context.GetSystemService(Context.ClipboardService).JavaCast<ClipboardManager>();
}
_clipboardManager = value;
}
public Task<string> GetTextAsync()
{
if (ClipboardManager.HasPrimaryClip)
if (_clipboardManager?.HasPrimaryClip == true)
{
return Task.FromResult<string>(ClipboardManager.PrimaryClip.GetItemAt(0).Text);
return Task.FromResult<string>(_clipboardManager.PrimaryClip.GetItemAt(0).Text);
}
return Task.FromResult<string>(null);
@@ -35,15 +29,25 @@ namespace Avalonia.Android.Platform
public Task SetTextAsync(string text)
{
if(_clipboardManager == null)
{
return Task.CompletedTask;
}
ClipData clip = ClipData.NewPlainText("text", text);
ClipboardManager.PrimaryClip = clip;
_clipboardManager.PrimaryClip = clip;
return Task.FromResult<object>(null);
}
public Task ClearAsync()
{
ClipboardManager.PrimaryClip = null;
if (_clipboardManager == null)
{
return Task.CompletedTask;
}
_clipboardManager.PrimaryClip = null;
return Task.FromResult<object>(null);
}
@@ -3,9 +3,13 @@ using System.Collections.Generic;
using Android.App;
using Android.Content;
using Android.Graphics;
using Android.Graphics.Drawables;
using Android.OS;
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;
@@ -13,6 +17,7 @@ using Avalonia.Controls;
using Avalonia.Controls.Platform;
using Avalonia.Controls.Platform.Surfaces;
using Avalonia.Input;
using Avalonia.Input.Platform;
using Avalonia.Input.Raw;
using Avalonia.Input.TextInput;
using Avalonia.OpenGL.Egl;
@@ -22,13 +27,7 @@ using Avalonia.Platform.Storage;
using Avalonia.Rendering;
using Avalonia.Rendering.Composition;
using Java.Lang;
using Java.Util;
using Math = System.Math;
using AndroidRect = Android.Graphics.Rect;
using Window = Android.Views.Window;
using Android.Graphics.Drawables;
using Android.OS;
using Android.Text;
using ClipboardManager = Android.Content.ClipboardManager;
namespace Avalonia.Android.Platform.SkiaPlatform
{
@@ -44,6 +43,7 @@ namespace Avalonia.Android.Platform.SkiaPlatform
private readonly IStorageProvider _storageProvider;
private readonly ISystemNavigationManagerImpl _systemNavigationManager;
private readonly AndroidInsetsManager _insetsManager;
private readonly ClipboardImpl _clipboard;
private ViewImpl _view;
public TopLevelImpl(AvaloniaView avaloniaView, bool placeOnTop = false)
@@ -54,6 +54,7 @@ namespace Avalonia.Android.Platform.SkiaPlatform
_pointerHelper = new AndroidMotionEventsHelper(this);
_gl = new EglGlPlatformSurface(this);
_framebuffer = new FramebufferManager(this);
_clipboard = new ClipboardImpl(avaloniaView.Context?.GetSystemService(Context.ClipboardService).JavaCast<ClipboardManager>());
RenderScaling = _view.Scaling;
@@ -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);
@@ -408,6 +411,11 @@ namespace Avalonia.Android.Platform.SkiaPlatform
return _insetsManager;
}
if(featureType == typeof(IClipboard))
{
return _clipboard;
}
return null;
}
}
@@ -8,7 +8,10 @@ using System.Threading.Tasks;
using Android;
using Android.App;
using Android.Content;
using Android.OS;
using Android.Provider;
using Android.Webkit;
using AndroidX.DocumentFile.Provider;
using Avalonia.Logging;
using Avalonia.Platform.Storage;
using Java.Lang;
@@ -22,20 +25,25 @@ internal abstract class AndroidStorageItem : IStorageBookmarkItem
{
private Activity? _activity;
private readonly bool _needsExternalFilesPermission;
private readonly AndroidStorageFolder? _parent;
private readonly AndroidUri? _permissionRoot;
protected AndroidStorageItem(Activity activity, AndroidUri uri, bool needsExternalFilesPermission)
protected AndroidStorageItem(Activity activity, AndroidUri uri, bool needsExternalFilesPermission, AndroidStorageFolder? parent = null, AndroidUri? permissionRoot = null)
{
_activity = activity;
_needsExternalFilesPermission = needsExternalFilesPermission;
_parent = parent;
_permissionRoot = permissionRoot ?? parent?.Uri ?? Uri;
Uri = uri;
}
internal AndroidUri Uri { get; }
internal AndroidUri Uri { get; set; }
protected Activity Activity => _activity ?? throw new ObjectDisposedException(nameof(AndroidStorageItem));
public virtual string Name => GetColumnValue(Activity, Uri, MediaStore.IMediaColumns.DisplayName)
?? Uri.PathSegments?.LastOrDefault() ?? string.Empty;
?? Document?.Name
?? Uri.PathSegments?.LastOrDefault()?.Split("/", StringSplitOptions.RemoveEmptyEntries).LastOrDefault() ?? string.Empty;
public Uri Path => new(Uri.ToString()!);
@@ -92,6 +100,23 @@ internal abstract class AndroidStorageItem : IStorageBookmarkItem
return null;
}
if(_parent != null)
{
return _parent;
}
var document = Document;
if (document == null)
{
return null;
}
if(document.ParentFile != null)
{
return new AndroidStorageFolder(Activity, document.ParentFile.Uri, true);
}
using var javaFile = new JavaFile(Uri.Path!);
// Java file represents files AND directories. Don't be confused.
@@ -118,35 +143,111 @@ internal abstract class AndroidStorageItem : IStorageBookmarkItem
{
_activity = null;
}
internal DocumentFile? Document
{
get
{
if (this is AndroidStorageFile)
{
return DocumentFile.FromSingleUri(Activity, Uri);
}
else
{
return DocumentFile.FromTreeUri(Activity, Uri);
}
}
}
internal AndroidUri? PermissionRoot => _permissionRoot;
public abstract Task DeleteAsync();
public abstract Task<IStorageItem?> MoveAsync(IStorageFolder destination);
}
internal class AndroidStorageFolder : AndroidStorageItem, IStorageBookmarkFolder
{
public AndroidStorageFolder(Activity activity, AndroidUri uri, bool needsExternalFilesPermission) : base(activity, uri, needsExternalFilesPermission)
public AndroidStorageFolder(Activity activity, AndroidUri uri, bool needsExternalFilesPermission, AndroidStorageFolder? parent = null, AndroidUri? permissionRoot = null) : base(activity, uri, needsExternalFilesPermission, parent, permissionRoot)
{
}
public async Task<IStorageFile?> CreateFileAsync(string name)
{
var mimeType = MimeTypeMap.Singleton?.GetMimeTypeFromExtension(MimeTypeMap.GetFileExtensionFromUrl(name)) ?? "application/octet-stream";
var newFile = Document.CreateFile(mimeType, name);
if(newFile == null)
{
return null;
}
return new AndroidStorageFile(Activity, newFile.Uri, this);
}
public async Task<IStorageFolder?> CreateFolderAsync(string name)
{
var newFolder = Document?.CreateDirectory(name);
if (newFolder == null)
{
return null;
}
return new AndroidStorageFolder(Activity, newFolder.Uri, false, this, PermissionRoot);
}
public override async Task DeleteAsync()
{
if (!await EnsureExternalFilesPermission(false))
{
return;
}
if (Activity != null)
{
await DeleteContents(this);
}
async Task DeleteContents(AndroidStorageFolder storageFolder)
{
await foreach (var file in storageFolder.GetItemsAsync())
{
if(file is AndroidStorageFolder folder)
{
await DeleteContents(folder);
}
else if(file is AndroidStorageFile storageFile)
{
await storageFile.DeleteAsync();
}
}
DocumentFile.FromTreeUri(Activity, storageFolder.Uri)?.Delete();
}
}
public override Task<StorageItemProperties> GetBasicPropertiesAsync()
{
return Task.FromResult(new StorageItemProperties());
}
public async Task<IReadOnlyList<IStorageItem>> GetItemsAsync()
public async IAsyncEnumerable<IStorageItem> GetItemsAsync()
{
if (!await EnsureExternalFilesPermission(false))
{
return Array.Empty<IStorageItem>();
yield break;
}
List<IStorageItem> files = new List<IStorageItem>();
var contentResolver = Activity.ContentResolver;
if (contentResolver == null)
{
return files;
yield break;
}
var childrenUri = DocumentsContract.BuildChildDocumentsUriUsingTree(Uri!, DocumentsContract.GetTreeDocumentId(Uri));
var root = PermissionRoot ?? Uri;
var folderId = root != Uri ? DocumentsContract.GetDocumentId(Uri) : DocumentsContract.GetTreeDocumentId(Uri);
var childrenUri = DocumentsContract.BuildChildDocumentsUriUsingTree(root, folderId);
var projection = new[]
{
@@ -162,19 +263,55 @@ internal class AndroidStorageFolder : AndroidStorageItem, IStorageBookmarkFolder
{
var mime = cursor.GetString(1);
var id = cursor.GetString(0);
var uri = DocumentsContract.BuildDocumentUriUsingTree(Uri!, id);
bool isDirectory = mime == DocumentsContract.Document.MimeTypeDir;
var uri = DocumentsContract.BuildDocumentUriUsingTree(root, id);
if (uri == null)
{
continue;
}
files.Add(mime == DocumentsContract.Document.MimeTypeDir ? new AndroidStorageFolder(Activity, uri, false) :
new AndroidStorageFile(Activity, uri));
yield return isDirectory ? new AndroidStorageFolder(Activity, uri, false, this, root) :
new AndroidStorageFile(Activity, uri, this, root);
}
}
}
return files;
}
public override async Task<IStorageItem?> MoveAsync(IStorageFolder destination)
{
if (Activity != null)
{
return await MoveRecursively(this, (AndroidStorageFolder)destination);
}
return null;
async Task<AndroidStorageFolder?> MoveRecursively(AndroidStorageFolder storageFolder, AndroidStorageFolder destination)
{
destination = await destination.CreateFolderAsync(storageFolder.Name) as AndroidStorageFolder;
if (destination == null)
{
return null;
}
await foreach (var file in storageFolder.GetItemsAsync())
{
if (file is AndroidStorageFolder folder)
{
await MoveRecursively(folder, destination);
}
else if (file is AndroidStorageFile)
{
await file.MoveAsync(destination);
}
}
await storageFolder.DeleteAsync();
return destination;
}
}
}
internal sealed class WellKnownAndroidStorageFolder : AndroidStorageFolder
@@ -186,14 +323,14 @@ internal sealed class WellKnownAndroidStorageFolder : AndroidStorageFolder
}
public override string Name { get; }
}
}
internal sealed class AndroidStorageFile : AndroidStorageItem, IStorageBookmarkFile
{
public AndroidStorageFile(Activity activity, AndroidUri uri) : base(activity, uri, false)
public AndroidStorageFile(Activity activity, AndroidUri uri, AndroidStorageFolder? parent = null, AndroidUri? permissionRoot = null) : base(activity, uri, false, parent, permissionRoot)
{
}
public Task<Stream> OpenReadAsync() => Task.FromResult(OpenContentStream(Activity, Uri, false)
?? throw new InvalidOperationException("Failed to open content stream"));
@@ -317,4 +454,76 @@ internal sealed class AndroidStorageFile : AndroidStorageItem, IStorageBookmarkF
return Task.FromResult(new StorageItemProperties(size, itemDate, dateModified));
}
public override async Task DeleteAsync()
{
if (!await EnsureExternalFilesPermission(false))
{
return;
}
if (Activity != null)
{
DocumentsContract.DeleteDocument(Activity.ContentResolver!, Uri);
}
}
public override async Task<IStorageItem?> MoveAsync(IStorageFolder destination)
{
if (!await EnsureExternalFilesPermission(false))
{
return null;
}
if (Activity != null && destination is AndroidStorageFolder storageFolder)
{
if (Build.VERSION.SdkInt >= BuildVersionCodes.N)
{
try
{
var uri = DocumentsContract.MoveDocument(Activity.ContentResolver!, Uri, ((await GetParentAsync()) as AndroidStorageFolder)!.Uri, storageFolder.Document!.Uri);
return new AndroidStorageFile(Activity, uri, storageFolder);
}
catch (Exception ex)
{
// There are many reason why DocumentContract will fail to move a file. We fallback to copying.
return await MoveFileByCopy();
}
}
else
{
return await MoveFileByCopy();
}
}
async Task<AndroidStorageFile?> MoveFileByCopy()
{
var newFile = await storageFolder.CreateFileAsync(Name) as AndroidStorageFile;
try
{
if (newFile != null)
{
using var input = await OpenReadAsync();
using var output = await newFile.OpenWriteAsync();
await input.CopyToAsync(output);
await DeleteAsync();
return new AndroidStorageFile(Activity, newFile.Uri, storageFolder);
}
}
catch
{
newFile?.DeleteAsync();
}
return null;
}
return null;
}
}
+34 -7
View File
@@ -1,6 +1,6 @@
using System;
using Avalonia.Reactive;
using Avalonia.Data;
using Avalonia.Reactive;
namespace Avalonia
{
@@ -34,8 +34,8 @@ namespace Avalonia
/// </remarks>
public static IObservable<object?> GetObservable(this AvaloniaObject o, AvaloniaProperty property)
{
return new AvaloniaPropertyObservable<object?>(
o ?? throw new ArgumentNullException(nameof(o)),
return new AvaloniaPropertyObservable<object?, object?>(
o ?? throw new ArgumentNullException(nameof(o)),
property ?? throw new ArgumentNullException(nameof(property)));
}
@@ -54,11 +54,23 @@ namespace Avalonia
/// </remarks>
public static IObservable<T> GetObservable<T>(this AvaloniaObject o, AvaloniaProperty<T> property)
{
return new AvaloniaPropertyObservable<T>(
return new AvaloniaPropertyObservable<T, T>(
o ?? throw new ArgumentNullException(nameof(o)),
property ?? throw new ArgumentNullException(nameof(property)));
}
/// <inheritdoc cref="GetObservable{T}(AvaloniaObject, AvaloniaProperty{T})"/>
/// <param name="o"/>
/// <param name="property"/>
/// <param name="converter">A method which is executed to convert each property value to <typeparamref name="TResult"/>.</param>
public static IObservable<TResult> GetObservable<TSource, TResult>(this AvaloniaObject o, AvaloniaProperty<TSource> property, Func<TSource, TResult> converter)
{
return new AvaloniaPropertyObservable<TSource, TResult>(
o ?? throw new ArgumentNullException(nameof(o)),
property ?? throw new ArgumentNullException(nameof(property)),
converter ?? throw new ArgumentNullException(nameof(converter)));
}
/// <summary>
/// Gets an observable for an <see cref="AvaloniaProperty"/>.
/// </summary>
@@ -75,7 +87,7 @@ namespace Avalonia
this AvaloniaObject o,
AvaloniaProperty property)
{
return new AvaloniaPropertyBindingObservable<object?>(
return new AvaloniaPropertyBindingObservable<object?, object?>(
o ?? throw new ArgumentNullException(nameof(o)),
property ?? throw new ArgumentNullException(nameof(property)));
}
@@ -97,12 +109,27 @@ namespace Avalonia
this AvaloniaObject o,
AvaloniaProperty<T> property)
{
return new AvaloniaPropertyBindingObservable<T>(
return new AvaloniaPropertyBindingObservable<T, T>(
o ?? throw new ArgumentNullException(nameof(o)),
property ?? throw new ArgumentNullException(nameof(property)));
}
/// <inheritdoc cref="GetBindingObservable{T}(AvaloniaObject, AvaloniaProperty{T})"/>
/// <param name="o"/>
/// <param name="property"/>
/// <param name="converter">A method which is executed to convert each property value to <typeparamref name="TResult"/>.</param>
public static IObservable<BindingValue<TResult>> GetBindingObservable<TSource, TResult>(
this AvaloniaObject o,
AvaloniaProperty<TSource> property,
Func<TSource, TResult> converter)
{
return new AvaloniaPropertyBindingObservable<TSource, TResult>(
o ?? throw new ArgumentNullException(nameof(o)),
property ?? throw new ArgumentNullException(nameof(property)),
converter ?? throw new ArgumentNullException(nameof(converter)));
}
/// <summary>
/// Gets an observable that listens for property changed events for an
/// <see cref="AvaloniaProperty"/>.
@@ -338,7 +365,7 @@ namespace Avalonia
return InstancedBinding.OneWay(_source);
}
}
private class ClassHandlerObserver<TTarget, TValue> : IObserver<AvaloniaPropertyChangedEventArgs<TValue>>
{
private readonly Action<TTarget, AvaloniaPropertyChangedEventArgs<TValue>> _action;
+6
View File
@@ -499,6 +499,12 @@ namespace Avalonia
/// <param name="o">The object instance.</param>
internal abstract void RouteClearValue(AvaloniaObject o);
/// <summary>
/// Routes an untyped CoerceValue call on a property with its default value to a typed call.
/// </summary>
/// <param name="o">The object instance.</param>
internal abstract void RouteCoerceDefaultValue(AvaloniaObject o);
/// <summary>
/// Routes an untyped GetValue call to a typed call.
/// </summary>
+5 -9
View File
@@ -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<IPlatformRenderInterface>();
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;
}
}
}
@@ -15,6 +15,7 @@ namespace Avalonia.Controls
/// </summary>
public class ResourceDictionary : IResourceDictionary
{
private object? lastDeferredItemKey;
private Dictionary<object, object?>? _inner;
private IResourceHost? _owner;
private AvaloniaList<IResourceProvider>? _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;
}
-9
View File
@@ -60,15 +60,6 @@ namespace Avalonia
/// </summary>
public double BottomLeft { get; }
/// <summary>
/// Gets a value indicating whether the instance has default values (all corner radii are set to 0).
/// </summary>
public bool IsDefault => TopLeft == 0 && TopRight == 0 && BottomLeft == 0 && BottomRight == 0;
/// <inheritdoc cref="IsDefault"/>
[Obsolete("Use IsDefault instead.")]
public bool IsEmpty => IsDefault;
/// <summary>
/// Gets a value indicating whether all corner radii are equal.
/// </summary>
@@ -40,7 +40,7 @@ namespace Avalonia.Data.Core
{
if (reference.TryGetTarget(out var target) && target is AvaloniaObject obj)
{
_subscription = new AvaloniaPropertyObservable<object?>(obj, _property).Subscribe(ValueChanged);
_subscription = new AvaloniaPropertyObservable<object?,object?>(obj, _property).Subscribe(ValueChanged);
}
else
{
+5
View File
@@ -117,6 +117,11 @@ namespace Avalonia
o.ClearValue<TValue>(this);
}
internal override void RouteCoerceDefaultValue(AvaloniaObject o)
{
// Do nothing.
}
/// <inheritdoc/>
internal override object? RouteGetValue(AvaloniaObject o)
{
@@ -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
/// <summary>
/// Defines the <see cref="CanHorizontallyScroll"/> property.
/// </summary>
public static readonly StyledProperty<bool> CanHorizontallyScrollProperty =
AvaloniaProperty.Register<ScrollGestureRecognizer, bool>(nameof(CanHorizontallyScroll));
public static readonly DirectProperty<ScrollGestureRecognizer, bool> CanHorizontallyScrollProperty =
AvaloniaProperty.RegisterDirect<ScrollGestureRecognizer, bool>(nameof(CanHorizontallyScroll),
o => o.CanHorizontallyScroll, (o, v) => o.CanHorizontallyScroll = v);
/// <summary>
/// Defines the <see cref="CanVerticallyScroll"/> property.
/// </summary>
public static readonly StyledProperty<bool> CanVerticallyScrollProperty =
AvaloniaProperty.Register<ScrollGestureRecognizer, bool>(nameof(CanVerticallyScroll));
public static readonly DirectProperty<ScrollGestureRecognizer, bool> CanVerticallyScrollProperty =
AvaloniaProperty.RegisterDirect<ScrollGestureRecognizer, bool>(nameof(CanVerticallyScroll),
o => o.CanVerticallyScroll, (o, v) => o.CanVerticallyScroll = v);
/// <summary>
/// Defines the <see cref="IsScrollInertiaEnabled"/> property.
/// </summary>
public static readonly StyledProperty<bool> IsScrollInertiaEnabledProperty =
AvaloniaProperty.Register<ScrollGestureRecognizer, bool>(nameof(IsScrollInertiaEnabled));
public static readonly DirectProperty<ScrollGestureRecognizer, bool> IsScrollInertiaEnabledProperty =
AvaloniaProperty.RegisterDirect<ScrollGestureRecognizer, bool>(nameof(IsScrollInertiaEnabled),
o => o.IsScrollInertiaEnabled, (o,v) => o.IsScrollInertiaEnabled = v);
/// <summary>
/// Defines the <see cref="ScrollStartDistance"/> property.
/// </summary>
public static readonly StyledProperty<int> ScrollStartDistanceProperty =
AvaloniaProperty.Register<ScrollGestureRecognizer, int>(nameof(ScrollStartDistance), 30);
public static readonly DirectProperty<ScrollGestureRecognizer, int> ScrollStartDistanceProperty =
AvaloniaProperty.RegisterDirect<ScrollGestureRecognizer, int>(nameof(ScrollStartDistance),
o => o.ScrollStartDistance, (o, v) => o.ScrollStartDistance = v,
unsetValue: 30);
/// <summary>
/// Gets or sets a value indicating whether the content can be scrolled horizontally.
/// </summary>
public bool CanHorizontallyScroll
{
get => GetValue(CanHorizontallyScrollProperty);
set => SetValue(CanHorizontallyScrollProperty, value);
get => _canHorizontallyScroll;
set => SetAndRaise(CanHorizontallyScrollProperty, ref _canHorizontallyScroll, value);
}
/// <summary>
@@ -63,17 +71,17 @@ namespace Avalonia.Input.GestureRecognizers
/// </summary>
public bool CanVerticallyScroll
{
get => GetValue(CanVerticallyScrollProperty);
set => SetValue(CanVerticallyScrollProperty, value);
get => _canVerticallyScroll;
set => SetAndRaise(CanVerticallyScrollProperty, ref _canVerticallyScroll, value);
}
/// <summary>
/// Gets or sets whether the gesture should include inertia in it's behavior.
/// </summary>
public bool IsScrollInertiaEnabled
{
get => GetValue(IsScrollInertiaEnabledProperty);
set => SetValue(IsScrollInertiaEnabledProperty, value);
get => _isScrollInertiaEnabled;
set => SetAndRaise(IsScrollInertiaEnabledProperty, ref _isScrollInertiaEnabled, value);
}
/// <summary>
@@ -81,10 +89,9 @@ namespace Avalonia.Input.GestureRecognizers
/// </summary>
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)
{
@@ -180,7 +180,7 @@ namespace Avalonia.Input.GestureRecognizers
internal Velocity GetVelocity()
{
var estimate = GetVelocityEstimate();
if (estimate == null || estimate.PixelsPerSecond.IsDefault)
if (estimate == null || estimate.PixelsPerSecond == default(Vector))
{
return new Velocity(Vector.Zero);
}
+18 -6
View File
@@ -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()
+1 -1
View File
@@ -125,7 +125,7 @@ namespace Avalonia.Layout
AvaloniaProperty.Register<Layoutable, VerticalAlignment>(nameof(VerticalAlignment));
/// <summary>
/// Defines the <see cref="UseLayoutRoundingProperty"/> property.
/// Defines the <see cref="UseLayoutRounding"/> property.
/// </summary>
public static readonly StyledProperty<bool> UseLayoutRoundingProperty =
AvaloniaProperty.Register<Layoutable, bool>(nameof(UseLayoutRounding), defaultValue: true, inherits: true);
+1 -10
View File
@@ -45,15 +45,6 @@ namespace Avalonia.Media
}
}
/// <summary>
/// Gets a value indicating whether the instance has default values.
/// </summary>
public bool IsDefault => OffsetX == 0 && OffsetY == 0 && Blur == 0 && Spread == 0;
/// <inheritdoc cref="IsDefault"/>
[Obsolete("Use IsDefault instead.")]
public bool IsEmpty => IsDefault;
private readonly static char[] s_Separator = new char[] { ' ', '\t' };
struct ArrayReader
@@ -89,7 +80,7 @@ namespace Avalonia.Media
{
var sb = StringBuilderCache.Acquire();
if (IsDefault)
if (this == default)
{
return "none";
}
+2 -2
View File
@@ -21,7 +21,7 @@ namespace Avalonia.Media
{
_first = shadow;
_list = null;
Count = _first.IsDefault ? 0 : 1;
Count = _first == default ? 0 : 1;
}
public BoxShadows(BoxShadow first, BoxShadow[] rest)
@@ -120,7 +120,7 @@ namespace Avalonia.Media
get
{
foreach(var boxShadow in this)
if (!boxShadow.IsDefault && boxShadow.IsInset)
if (boxShadow != default && boxShadow.IsInset)
return true;
return false;
}
+1 -1
View File
@@ -309,7 +309,7 @@ namespace Avalonia.Media
if (input.Length == 3 || input.Length == 4)
{
var extendedLength = 2 * input.Length;
#if !BUILDTASK
Span<char> extended = stackalloc char[extendedLength];
#else
+2 -2
View File
@@ -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
/// </remarks>
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);
}
+1 -1
View File
@@ -73,7 +73,7 @@ namespace Avalonia.Media
{
var bounds = GetBounds();
using (context.PushPreTransform(Transform?.Value ?? Matrix.Identity))
using (context.PushTransform(Transform?.Value ?? Matrix.Identity))
using (context.PushOpacity(Opacity, bounds))
using (ClipGeometry != null ? context.PushGeometryClip(ClipGeometry) : default)
using (OpacityMask != null ? context.PushOpacityMask(OpacityMask, bounds) : default)
@@ -0,0 +1,22 @@
using System;
// ReSharper disable CheckNamespace
namespace Avalonia.Media;
public class BlurEffect : Effect, IBlurEffect, IMutableEffect
{
public static readonly StyledProperty<double> RadiusProperty = AvaloniaProperty.Register<BlurEffect, double>(
nameof(Radius), 5);
public double Radius
{
get => GetValue(RadiusProperty);
set => SetValue(RadiusProperty, value);
}
static BlurEffect()
{
AffectsRender<BlurEffect>(RadiusProperty);
}
public IImmutableEffect ToImmutable() => new ImmutableBlurEffect(Radius);
}
@@ -0,0 +1,104 @@
// ReSharper disable once CheckNamespace
using System;
// ReSharper disable CheckNamespace
namespace Avalonia.Media;
public abstract class DropShadowEffectBase : Effect
{
public static readonly StyledProperty<double> BlurRadiusProperty =
AvaloniaProperty.Register<DropShadowEffectBase, double>(
nameof(BlurRadius), 5);
public double BlurRadius
{
get => GetValue(BlurRadiusProperty);
set => SetValue(BlurRadiusProperty, value);
}
public static readonly StyledProperty<Color> ColorProperty = AvaloniaProperty.Register<DropShadowEffectBase, Color>(
nameof(Color));
public Color Color
{
get => GetValue(ColorProperty);
set => SetValue(ColorProperty, value);
}
public static readonly StyledProperty<double> OpacityProperty =
AvaloniaProperty.Register<DropShadowEffectBase, double>(
nameof(Opacity), 1);
public double Opacity
{
get => GetValue(OpacityProperty);
set => SetValue(OpacityProperty, value);
}
static DropShadowEffectBase()
{
AffectsRender<DropShadowEffectBase>(BlurRadiusProperty, ColorProperty, OpacityProperty);
}
}
public class DropShadowEffect : DropShadowEffectBase, IDropShadowEffect, IMutableEffect
{
public static readonly StyledProperty<double> OffsetXProperty = AvaloniaProperty.Register<DropShadowEffect, double>(
nameof(OffsetX), 3.5355);
public double OffsetX
{
get => GetValue(OffsetXProperty);
set => SetValue(OffsetXProperty, value);
}
public static readonly StyledProperty<double> OffsetYProperty = AvaloniaProperty.Register<DropShadowEffect, double>(
nameof(OffsetY), 3.5355);
public double OffsetY
{
get => GetValue(OffsetYProperty);
set => SetValue(OffsetYProperty, value);
}
static DropShadowEffect()
{
AffectsRender<DropShadowEffect>(OffsetXProperty, OffsetYProperty);
}
public IImmutableEffect ToImmutable()
{
return new ImmutableDropShadowEffect(OffsetX, OffsetY, BlurRadius, Color, Opacity);
}
}
/// <summary>
/// This class is compatible with WPF's DropShadowEffect and provides Direction and ShadowDepth properties instead of OffsetX/OffsetY
/// </summary>
public class DropShadowDirectionEffect : DropShadowEffectBase, IDirectionDropShadowEffect, IMutableEffect
{
public static readonly StyledProperty<double> ShadowDepthProperty =
AvaloniaProperty.Register<DropShadowDirectionEffect, double>(
nameof(ShadowDepth), 5);
public double ShadowDepth
{
get => GetValue(ShadowDepthProperty);
set => SetValue(ShadowDepthProperty, value);
}
public static readonly StyledProperty<double> DirectionProperty = AvaloniaProperty.Register<DropShadowDirectionEffect, double>(
nameof(Direction), 315);
public double Direction
{
get => GetValue(DirectionProperty);
set => SetValue(DirectionProperty, value);
}
public double OffsetX => Math.Cos(Direction) * ShadowDepth;
public double OffsetY => Math.Sin(Direction) * ShadowDepth;
public IImmutableEffect ToImmutable() => new ImmutableDropShadowDirectionEffect(OffsetX, OffsetY, BlurRadius, Color, Opacity);
}
+93
View File
@@ -0,0 +1,93 @@
using System;
using Avalonia.Animation;
using Avalonia.Animation.Animators;
using Avalonia.Reactive;
using Avalonia.Rendering.Composition.Expressions;
using Avalonia.Utilities;
// ReSharper disable once CheckNamespace
namespace Avalonia.Media;
public class Effect : Animatable, IAffectsRender
{
/// <summary>
/// Marks a property as affecting the brush's visual representation.
/// </summary>
/// <param name="properties">The properties.</param>
/// <remarks>
/// After a call to this method in a brush's static constructor, any change to the
/// property will cause the <see cref="Invalidated"/> event to be raised on the brush.
/// </remarks>
protected static void AffectsRender<T>(params AvaloniaProperty[] properties)
where T : Effect
{
var invalidateObserver = new AnonymousObserver<AvaloniaPropertyChangedEventArgs>(
static e => (e.Sender as T)?.RaiseInvalidated(EventArgs.Empty));
foreach (var property in properties)
{
property.Changed.Subscribe(invalidateObserver);
}
}
/// <summary>
/// Raises the <see cref="Invalidated"/> event.
/// </summary>
/// <param name="e">The event args.</param>
protected void RaiseInvalidated(EventArgs e) => Invalidated?.Invoke(this, e);
/// <inheritdoc />
public event EventHandler? Invalidated;
static Exception ParseError(string s) => throw new ArgumentException("Unable to parse effect: " + s);
public static IEffect Parse(string s)
{
var span = s.AsSpan();
var r = new TokenParser(span);
if (r.TryConsume("blur"))
{
if (!r.TryConsume('(') || !r.TryParseDouble(out var radius) || !r.TryConsume(')') || !r.IsEofWithWhitespace())
throw ParseError(s);
return new ImmutableBlurEffect(radius);
}
if (r.TryConsume("drop-shadow"))
{
if (!r.TryConsume('(') || !r.TryParseDouble(out var offsetX)
|| !r.TryParseDouble(out var offsetY))
throw ParseError(s);
double blurRadius = 0;
var color = Colors.Black;
if (!r.TryConsume(')'))
{
if (!r.TryParseDouble(out blurRadius) || blurRadius < 0)
throw ParseError(s);
if (!r.TryConsume(')'))
{
var endOfExpression = s.LastIndexOf(")", StringComparison.Ordinal);
if (endOfExpression == -1)
throw ParseError(s);
if (!new TokenParser(span.Slice(endOfExpression + 1)).IsEofWithWhitespace())
throw ParseError(s);
if (!Color.TryParse(span.Slice(r.Position, endOfExpression - r.Position).TrimEnd(), out color))
throw ParseError(s);
return new ImmutableDropShadowEffect(offsetX, offsetY, blurRadius, color, 1);
}
}
if (!r.IsEofWithWhitespace())
throw ParseError(s);
return new ImmutableDropShadowEffect(offsetX, offsetY, blurRadius, color, 1);
}
throw ParseError(s);
}
static Effect()
{
EffectAnimator.EnsureRegistered();
}
}
@@ -0,0 +1,131 @@
using System;
using System.Diagnostics.CodeAnalysis;
using Avalonia.Data;
using Avalonia.Logging;
using Avalonia.Media;
// ReSharper disable once CheckNamespace
namespace Avalonia.Animation.Animators;
public class EffectAnimator : Animator<IEffect?>
{
public override IDisposable? Apply(Animation animation, Animatable control, IClock? clock,
IObservable<bool> match, Action? onComplete)
{
if (TryCreateAnimator<BlurEffectAnimator, IBlurEffect>(out var animator)
|| TryCreateAnimator<DropShadowEffectAnimator, IDropShadowEffect>(out animator))
return animator.Apply(animation, control, clock, match, onComplete);
Logger.TryGet(LogEventLevel.Error, LogArea.Animations)?.Log(
this,
"The animation's keyframe value types set is not supported.");
return base.Apply(animation, control, clock, match, onComplete);
}
private bool TryCreateAnimator<TAnimator, TInterface>([NotNullWhen(true)] out IAnimator? animator)
where TAnimator : EffectAnimatorBase<TInterface>, new() where TInterface : class, IEffect
{
TAnimator? createdAnimator = null;
foreach (var keyFrame in this)
{
if (keyFrame.Value is TInterface)
{
createdAnimator ??= new TAnimator()
{
Property = Property
};
createdAnimator.Add(new AnimatorKeyFrame(typeof(TAnimator), () => new TAnimator(), keyFrame.Cue,
keyFrame.KeySpline)
{
Value = keyFrame.Value
});
}
else
{
animator = null;
return false;
}
}
animator = createdAnimator;
return animator != null;
}
/// <summary>
/// Fallback implementation of <see cref="IEffect"/> animation.
/// </summary>
public override IEffect? Interpolate(double progress, IEffect? oldValue, IEffect? newValue) => progress >= 0.5 ? newValue : oldValue;
private static bool s_Registered;
public static void EnsureRegistered()
{
if(s_Registered)
return;
s_Registered = true;
Animation.RegisterAnimator<EffectAnimator>(prop =>
typeof(IEffect).IsAssignableFrom(prop.PropertyType));
}
}
public abstract class EffectAnimatorBase<T> : Animator<IEffect?> where T : class, IEffect?
{
public override IDisposable BindAnimation(Animatable control, IObservable<IEffect?> instance)
{
if (Property is null)
{
throw new InvalidOperationException("Animator has no property specified.");
}
return control.Bind((AvaloniaProperty<IEffect?>)Property, instance, BindingPriority.Animation);
}
protected abstract T Interpolate(double progress, T oldValue, T newValue);
public override IEffect? Interpolate(double progress, IEffect? oldValue, IEffect? newValue)
{
var old = oldValue as T;
var n = newValue as T;
if (old == null || n == null)
return progress >= 0.5 ? newValue : oldValue;
return Interpolate(progress, old, n);
}
}
public class BlurEffectAnimator : EffectAnimatorBase<IBlurEffect>
{
private static readonly DoubleAnimator s_doubleAnimator = new DoubleAnimator();
protected override IBlurEffect Interpolate(double progress, IBlurEffect oldValue, IBlurEffect newValue)
{
return new ImmutableBlurEffect(
s_doubleAnimator.Interpolate(progress, oldValue.Radius, newValue.Radius));
}
}
public class DropShadowEffectAnimator : EffectAnimatorBase<IDropShadowEffect>
{
private static readonly DoubleAnimator s_doubleAnimator = new DoubleAnimator();
protected override IDropShadowEffect Interpolate(double progress, IDropShadowEffect oldValue,
IDropShadowEffect newValue)
{
var blur = s_doubleAnimator.Interpolate(progress, oldValue.BlurRadius, newValue.BlurRadius);
var color = ColorAnimator.InterpolateCore(progress, oldValue.Color, newValue.Color);
var opacity = s_doubleAnimator.Interpolate(progress, oldValue.Opacity, newValue.Opacity);
if (oldValue is IDirectionDropShadowEffect oldDirection && newValue is IDirectionDropShadowEffect newDirection)
{
return new ImmutableDropShadowDirectionEffect(
s_doubleAnimator.Interpolate(progress, oldDirection.Direction, newDirection.Direction),
s_doubleAnimator.Interpolate(progress, oldDirection.ShadowDepth, newDirection.ShadowDepth),
blur, color, opacity
);
}
return new ImmutableDropShadowEffect(
s_doubleAnimator.Interpolate(progress, oldValue.OffsetX, newValue.OffsetX),
s_doubleAnimator.Interpolate(progress, oldValue.OffsetY, newValue.OffsetY),
blur, color, opacity
);
}
}
@@ -0,0 +1,18 @@
using System;
using System.ComponentModel;
using System.Globalization;
namespace Avalonia.Media;
public class EffectConverter : TypeConverter
{
public override bool CanConvertFrom(ITypeDescriptorContext? context, Type sourceType)
{
return sourceType == typeof(string);
}
public override object? ConvertFrom(ITypeDescriptorContext? context, CultureInfo? culture, object? value)
{
return value is string s ? Effect.Parse(s) : null;
}
}
@@ -0,0 +1,56 @@
using System;
// ReSharper disable once CheckNamespace
namespace Avalonia.Media;
public static class EffectExtensions
{
static double AdjustPaddingRadius(double radius)
{
if (radius <= 0)
return 0;
return Math.Ceiling(radius) + 1;
}
internal static Thickness GetEffectOutputPadding(this IEffect? effect)
{
if (effect == null)
return default;
if (effect is IBlurEffect blur)
return new Thickness(AdjustPaddingRadius(blur.Radius));
if (effect is IDropShadowEffect dropShadowEffect)
{
var radius = AdjustPaddingRadius(dropShadowEffect.BlurRadius);
var rc = new Rect(-radius, -radius,
radius * 2, radius * 2);
rc = rc.Translate(new(dropShadowEffect.OffsetX, dropShadowEffect.OffsetY));
return new Thickness(Math.Max(0, 0 - rc.X),
Math.Max(0, 0 - rc.Y), Math.Max(0, rc.Right), Math.Max(0, rc.Bottom));
}
throw new ArgumentException("Unknown effect type: " + effect.GetType());
}
/// <summary>
/// Converts a effect to an immutable effect.
/// </summary>
/// <param name="effect">The effect.</param>
/// <returns>
/// The result of calling <see cref="IMutableEffect.ToImmutable"/> if the effect is mutable,
/// otherwise <paramref name="effect"/>.
/// </returns>
public static IImmutableEffect ToImmutable(this IEffect effect)
{
_ = effect ?? throw new ArgumentNullException(nameof(effect));
return (effect as IMutableEffect)?.ToImmutable() ?? (IImmutableEffect)effect;
}
internal static bool EffectEquals(this IImmutableEffect? immutable, IEffect? right)
{
if (immutable == null && right == null)
return true;
if (immutable != null && right != null)
return immutable.Equals(right);
return false;
}
}

Some files were not shown because too many files have changed in this diff Show More