committed by
GitHub
468 changed files with 15537 additions and 8126 deletions
@ -0,0 +1,4 @@ |
|||
<?xml version="1.0" encoding="utf-8"?> |
|||
<resources> |
|||
<color name="splash_background">#212121</color> |
|||
</resources> |
|||
@ -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 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> |
|||
</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); |
|||
} |
|||
} |
|||
} |
|||
|
After Width: | Height: | Size: 14 KiB |
@ -0,0 +1,11 @@ |
|||
using Android.App; |
|||
using Android.Content.PM; |
|||
using Avalonia.Android; |
|||
|
|||
namespace SafeAreaDemo.Android |
|||
{ |
|||
[Activity(Label = "SafeAreaDemo.Android", Theme = "@style/MyTheme.NoActionBar", Icon = "@drawable/icon", LaunchMode = LaunchMode.SingleTop, ConfigurationChanges = ConfigChanges.Orientation | ConfigChanges.ScreenSize | ConfigChanges.UiMode)] |
|||
public class MainActivity : AvaloniaMainActivity |
|||
{ |
|||
} |
|||
} |
|||
@ -0,0 +1,5 @@ |
|||
<?xml version="1.0" encoding="utf-8"?> |
|||
<manifest xmlns:android="http://schemas.android.com/apk/res/android" android:installLocation="auto"> |
|||
<uses-permission android:name="android.permission.INTERNET" /> |
|||
<application android:label="SafeAreaDemo" android:icon="@drawable/Icon" /> |
|||
</manifest> |
|||
@ -0,0 +1,13 @@ |
|||
<?xml version="1.0" encoding="utf-8"?> |
|||
<layer-list xmlns:android="http://schemas.android.com/apk/res/android"> |
|||
|
|||
<item> |
|||
<color android:color="@color/splash_background"/> |
|||
</item> |
|||
|
|||
<item android:drawable="@drawable/icon" |
|||
android:width="120dp" |
|||
android:height="120dp" |
|||
android:gravity="center" /> |
|||
|
|||
</layer-list> |
|||
@ -0,0 +1,4 @@ |
|||
<?xml version="1.0" encoding="utf-8"?> |
|||
<resources> |
|||
<color name="splash_background">#FFFFFF</color> |
|||
</resources> |
|||
@ -0,0 +1,17 @@ |
|||
<?xml version="1.0" encoding="utf-8" ?> |
|||
<resources> |
|||
|
|||
<style name="MyTheme"> |
|||
</style> |
|||
|
|||
<style name="MyTheme.NoActionBar" parent="@style/Theme.AppCompat.NoActionBar"> |
|||
<item name="android:windowActionBar">false</item> |
|||
<item name="android:windowNoTitle">true</item> |
|||
</style> |
|||
|
|||
<style name="MyTheme.Splash" parent ="MyTheme.NoActionBar"> |
|||
<item name="android:windowBackground">@drawable/splash_screen</item> |
|||
<item name="android:windowContentOverlay">@null</item> |
|||
</style> |
|||
|
|||
</resources> |
|||
@ -0,0 +1,24 @@ |
|||
<Project Sdk="Microsoft.NET.Sdk"> |
|||
<PropertyGroup> |
|||
<OutputType>Exe</OutputType> |
|||
<TargetFramework>net7.0-android</TargetFramework> |
|||
<SupportedOSPlatformVersion>21</SupportedOSPlatformVersion> |
|||
<Nullable>enable</Nullable> |
|||
<ApplicationId>com.avalonia.safeareademo</ApplicationId> |
|||
<ApplicationVersion>1</ApplicationVersion> |
|||
<ApplicationDisplayVersion>1.0</ApplicationDisplayVersion> |
|||
<AndroidPackageFormat>apk</AndroidPackageFormat> |
|||
<AndroidEnableProfiledAot>False</AndroidEnableProfiledAot> |
|||
</PropertyGroup> |
|||
|
|||
<ItemGroup> |
|||
<AndroidResource Include="Icon.png"> |
|||
<Link>Resources\drawable\Icon.png</Link> |
|||
</AndroidResource> |
|||
</ItemGroup> |
|||
|
|||
<ItemGroup> |
|||
<ProjectReference Include="..\SafeAreaDemo\SafeAreaDemo.csproj" /> |
|||
<ProjectReference Include="..\..\src\Android\Avalonia.Android\Avalonia.Android.csproj" /> |
|||
</ItemGroup> |
|||
</Project> |
|||
@ -0,0 +1,30 @@ |
|||
using Android.App; |
|||
using Android.Content; |
|||
using Android.OS; |
|||
using Avalonia; |
|||
using Avalonia.Android; |
|||
using Application = Android.App.Application; |
|||
|
|||
namespace SafeAreaDemo.Android |
|||
{ |
|||
[Activity(Theme = "@style/MyTheme.Splash", MainLauncher = true, NoHistory = true)] |
|||
public class SplashActivity : AvaloniaSplashActivity<App> |
|||
{ |
|||
protected override AppBuilder CustomizeAppBuilder(AppBuilder builder) |
|||
{ |
|||
return base.CustomizeAppBuilder(builder); |
|||
} |
|||
|
|||
protected override void OnCreate(Bundle? savedInstanceState) |
|||
{ |
|||
base.OnCreate(savedInstanceState); |
|||
} |
|||
|
|||
protected override void OnResume() |
|||
{ |
|||
base.OnResume(); |
|||
|
|||
StartActivity(new Intent(Application.Context, typeof(MainActivity))); |
|||
} |
|||
} |
|||
} |
|||
@ -0,0 +1,21 @@ |
|||
using Avalonia; |
|||
using System; |
|||
|
|||
namespace SafeAreaDemo.Desktop |
|||
{ |
|||
internal class Program |
|||
{ |
|||
// Initialization code. Don't use any Avalonia, third-party APIs or any
|
|||
// SynchronizationContext-reliant code before AppMain is called: things aren't initialized
|
|||
// yet and stuff might break.
|
|||
[STAThread] |
|||
public static void Main(string[] args) => BuildAvaloniaApp() |
|||
.StartWithClassicDesktopLifetime(args); |
|||
|
|||
// Avalonia configuration, don't remove; also used by visual designer.
|
|||
public static AppBuilder BuildAvaloniaApp() |
|||
=> AppBuilder.Configure<App>() |
|||
.UsePlatformDetect() |
|||
.LogToTrace(); |
|||
} |
|||
} |
|||
@ -0,0 +1,24 @@ |
|||
<Project Sdk="Microsoft.NET.Sdk"> |
|||
<PropertyGroup> |
|||
<OutputType>WinExe</OutputType> |
|||
<!--If you are willing to use Windows/MacOS native APIs you will need to create 3 projects. |
|||
One for Windows with net7.0-windows TFM, one for MacOS with net7.0-macos and one with net7.0 TFM for Linux.--> |
|||
<TargetFramework>net7.0</TargetFramework> |
|||
<Nullable>enable</Nullable> |
|||
<BuiltInComInteropSupport>true</BuiltInComInteropSupport> |
|||
</PropertyGroup> |
|||
|
|||
<PropertyGroup> |
|||
<ApplicationManifest>app.manifest</ApplicationManifest> |
|||
</PropertyGroup> |
|||
|
|||
<ItemGroup> |
|||
<ProjectReference Include="..\..\src\Avalonia.Diagnostics\Avalonia.Diagnostics.csproj" /> |
|||
<ProjectReference Include="..\..\src\Avalonia.Headless.Vnc\Avalonia.Headless.Vnc.csproj" /> |
|||
<ProjectReference Include="..\..\src\Linux\Avalonia.LinuxFramebuffer\Avalonia.LinuxFramebuffer.csproj" /> |
|||
<ProjectReference Include="..\..\src\Avalonia.X11\Avalonia.X11.csproj" /> |
|||
<ProjectReference Include="..\SafeAreaDemo\SafeAreaDemo.csproj" /> |
|||
</ItemGroup> |
|||
<Import Project="..\..\build\SampleApp.props" /> |
|||
<Import Project="..\..\build\ReferenceCoreLibraries.props" /> |
|||
</Project> |
|||
@ -0,0 +1,18 @@ |
|||
<?xml version="1.0" encoding="utf-8"?> |
|||
<assembly manifestVersion="1.0" xmlns="urn:schemas-microsoft-com:asm.v1"> |
|||
<!-- This manifest is used on Windows only. |
|||
Don't remove it as it might cause problems with window transparency and embeded controls. |
|||
For more details visit https://learn.microsoft.com/en-us/windows/win32/sbscs/application-manifests --> |
|||
<assemblyIdentity version="1.0.0.0" name="SafeAreaDemo.Desktop"/> |
|||
|
|||
<compatibility xmlns="urn:schemas-microsoft-com:compatibility.v1"> |
|||
<application> |
|||
<!-- A list of the Windows versions that this application has been tested on |
|||
and is designed to work with. Uncomment the appropriate elements |
|||
and Windows will automatically select the most compatible environment. --> |
|||
|
|||
<!-- Windows 10 --> |
|||
<supportedOS Id="{8e0f7a12-bfb3-4fe8-b9a5-48fd50a15a9a}" /> |
|||
</application> |
|||
</compatibility> |
|||
</assembly> |
|||
@ -0,0 +1,17 @@ |
|||
using Avalonia; |
|||
using Avalonia.Controls; |
|||
using Avalonia.iOS; |
|||
using Avalonia.Media; |
|||
using Foundation; |
|||
using UIKit; |
|||
|
|||
namespace SafeAreaDemo.iOS |
|||
{ |
|||
// The UIApplicationDelegate for the application. This class is responsible for launching the
|
|||
// User Interface of the application, as well as listening (and optionally responding) to
|
|||
// application events from iOS.
|
|||
[Register("AppDelegate")] |
|||
public partial class AppDelegate : AvaloniaAppDelegate<App> |
|||
{ |
|||
} |
|||
} |
|||
@ -0,0 +1,5 @@ |
|||
<?xml version="1.0" encoding="UTF-8"?> |
|||
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd"> |
|||
<plist version="1.0"> |
|||
<dict/> |
|||
</plist> |
|||
@ -0,0 +1,47 @@ |
|||
<?xml version="1.0" encoding="UTF-8"?> |
|||
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd"> |
|||
<plist version="1.0"> |
|||
<dict> |
|||
<key>CFBundleDisplayName</key> |
|||
<string>SafeAreaDemo</string> |
|||
<key>CFBundleIdentifier</key> |
|||
<string>companyName.SafeAreaDemo</string> |
|||
<key>CFBundleShortVersionString</key> |
|||
<string>1.0</string> |
|||
<key>CFBundleVersion</key> |
|||
<string>1.0</string> |
|||
<key>LSRequiresIPhoneOS</key> |
|||
<true/> |
|||
<key>MinimumOSVersion</key> |
|||
<string>10.0</string> |
|||
<key>UIDeviceFamily</key> |
|||
<array> |
|||
<integer>1</integer> |
|||
<integer>2</integer> |
|||
</array> |
|||
<key>UILaunchStoryboardName</key> |
|||
<string>LaunchScreen</string> |
|||
<key>UIRequiredDeviceCapabilities</key> |
|||
<array> |
|||
<string>armv7</string> |
|||
</array> |
|||
<key>UISupportedInterfaceOrientations</key> |
|||
<array> |
|||
<string>UIInterfaceOrientationPortrait</string> |
|||
<string>UIInterfaceOrientationPortraitUpsideDown</string> |
|||
<string>UIInterfaceOrientationLandscapeLeft</string> |
|||
<string>UIInterfaceOrientationLandscapeRight</string> |
|||
</array> |
|||
<key>UISupportedInterfaceOrientations~ipad</key> |
|||
<array> |
|||
<string>UIInterfaceOrientationPortrait</string> |
|||
<string>UIInterfaceOrientationPortraitUpsideDown</string> |
|||
<string>UIInterfaceOrientationLandscapeLeft</string> |
|||
<string>UIInterfaceOrientationLandscapeRight</string> |
|||
</array> |
|||
<key>UIStatusBarHidden</key> |
|||
<true/> |
|||
<key>UIViewControllerBasedStatusBarAppearance</key> |
|||
<false/> |
|||
</dict> |
|||
</plist> |
|||
@ -0,0 +1,15 @@ |
|||
using UIKit; |
|||
|
|||
namespace SafeAreaDemo.iOS |
|||
{ |
|||
public class Application |
|||
{ |
|||
// This is the main entry point of the application.
|
|||
static void Main(string[] args) |
|||
{ |
|||
// if you want to use a different Application Delegate class from "AppDelegate"
|
|||
// you can specify it here.
|
|||
UIApplication.Main(args, null, typeof(AppDelegate)); |
|||
} |
|||
} |
|||
} |
|||
@ -0,0 +1,43 @@ |
|||
<?xml version="1.0" encoding="UTF-8" standalone="no"?> |
|||
<document type="com.apple.InterfaceBuilder3.CocoaTouch.XIB" version="3.0" toolsVersion="6214" systemVersion="14A314h" targetRuntime="iOS.CocoaTouch" propertyAccessControl="none" useAutolayout="YES" launchScreen="YES" useTraitCollections="YES"> |
|||
<dependencies> |
|||
<plugIn identifier="com.apple.InterfaceBuilder.IBCocoaTouchPlugin" version="6207" /> |
|||
<capability name="Constraints with non-1.0 multipliers" minToolsVersion="5.1" /> |
|||
</dependencies> |
|||
<objects> |
|||
<placeholder placeholderIdentifier="IBFilesOwner" id="-1" userLabel="File's Owner" /> |
|||
<placeholder placeholderIdentifier="IBFirstResponder" id="-2" customClass="UIResponder" /> |
|||
<view contentMode="scaleToFill" id="iN0-l3-epB"> |
|||
<rect key="frame" x="0.0" y="0.0" width="480" height="480" /> |
|||
<autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES" /> |
|||
<subviews> |
|||
<label opaque="NO" clipsSubviews="YES" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="251" text=" Copyright (c) 2022 " textAlignment="center" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" |
|||
minimumFontSize="9" translatesAutoresizingMaskIntoConstraints="NO" id="8ie-xW-0ye"> |
|||
<rect key="frame" x="20" y="439" width="441" height="21" /> |
|||
<fontDescription key="fontDescription" type="system" pointSize="17" /> |
|||
<color key="textColor" cocoaTouchSystemColor="darkTextColor" /> |
|||
<nil key="highlightedColor" /> |
|||
</label> |
|||
<label opaque="NO" clipsSubviews="YES" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="251" text="SafeAreaDemo" textAlignment="center" lineBreakMode="middleTruncation" baselineAdjustment="alignBaselines" |
|||
minimumFontSize="18" translatesAutoresizingMaskIntoConstraints="NO" id="kId-c2-rCX"> |
|||
<rect key="frame" x="20" y="140" width="441" height="43" /> |
|||
<fontDescription key="fontDescription" type="boldSystem" pointSize="36" /> |
|||
<color key="textColor" cocoaTouchSystemColor="darkTextColor" /> |
|||
<nil key="highlightedColor" /> |
|||
</label> |
|||
</subviews> |
|||
<color key="backgroundColor" white="1" alpha="1" colorSpace="custom" customColorSpace="calibratedWhite" /> |
|||
<constraints> |
|||
<constraint firstItem="kId-c2-rCX" firstAttribute="centerY" secondItem="iN0-l3-epB" secondAttribute="bottom" multiplier="1/3" constant="1" id="5cJ-9S-tgC" /> |
|||
<constraint firstAttribute="centerX" secondItem="kId-c2-rCX" secondAttribute="centerX" id="Koa-jz-hwk" /> |
|||
<constraint firstAttribute="bottom" secondItem="8ie-xW-0ye" secondAttribute="bottom" constant="20" id="Kzo-t9-V3l" /> |
|||
<constraint firstItem="8ie-xW-0ye" firstAttribute="leading" secondItem="iN0-l3-epB" secondAttribute="leading" constant="20" symbolic="YES" id="MfP-vx-nX0" /> |
|||
<constraint firstAttribute="centerX" secondItem="8ie-xW-0ye" secondAttribute="centerX" id="ZEH-qu-HZ9" /> |
|||
<constraint firstItem="kId-c2-rCX" firstAttribute="leading" secondItem="iN0-l3-epB" secondAttribute="leading" constant="20" symbolic="YES" id="fvb-Df-36g" /> |
|||
</constraints> |
|||
<nil key="simulatedStatusBarMetrics" /> |
|||
<freeformSimulatedSizeMetrics key="simulatedDestinationMetrics" /> |
|||
<point key="canvasLocation" x="548" y="455" /> |
|||
</view> |
|||
</objects> |
|||
</document> |
|||
@ -0,0 +1,18 @@ |
|||
<Project Sdk="Microsoft.NET.Sdk"> |
|||
<PropertyGroup> |
|||
<OutputType>Exe</OutputType> |
|||
<TargetFramework>net7.0-ios</TargetFramework> |
|||
<SupportedOSPlatformVersion>10.0</SupportedOSPlatformVersion> |
|||
<ProvisioningType>manual</ProvisioningType> |
|||
<Nullable>enable</Nullable> |
|||
<RuntimeIdentifier>iossimulator-x64</RuntimeIdentifier> |
|||
|
|||
<!-- These properties need to be set in order to run on a real iDevice --> |
|||
<!--<RuntimeIdentifier>ios-arm64</RuntimeIdentifier>--> |
|||
<!--<CodesignKey></CodesignKey>--> |
|||
</PropertyGroup> |
|||
<ItemGroup> |
|||
<ProjectReference Include="..\SafeAreaDemo\SafeAreaDemo.csproj" /> |
|||
<ProjectReference Include="..\..\src\iOS\Avalonia.iOS\Avalonia.iOS.csproj" /> |
|||
</ItemGroup> |
|||
</Project> |
|||
@ -0,0 +1,15 @@ |
|||
<Application xmlns="https://github.com/avaloniaui" |
|||
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" |
|||
xmlns:local="using:SafeAreaDemo" |
|||
x:Class="SafeAreaDemo.App" |
|||
RequestedThemeVariant="Default"> |
|||
<!-- "Default" ThemeVariant follows system theme variant. "Dark" or "Light" are other available options. --> |
|||
|
|||
<Application.DataTemplates> |
|||
<local:ViewLocator/> |
|||
</Application.DataTemplates> |
|||
|
|||
<Application.Styles> |
|||
<FluentTheme /> |
|||
</Application.Styles> |
|||
</Application> |
|||
@ -0,0 +1,36 @@ |
|||
using Avalonia; |
|||
using Avalonia.Controls.ApplicationLifetimes; |
|||
using Avalonia.Markup.Xaml; |
|||
using SafeAreaDemo.ViewModels; |
|||
using SafeAreaDemo.Views; |
|||
|
|||
namespace SafeAreaDemo |
|||
{ |
|||
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 |
|||
{ |
|||
DataContext = new MainViewModel() |
|||
}; |
|||
} |
|||
else if (ApplicationLifetime is ISingleViewApplicationLifetime singleViewPlatform) |
|||
{ |
|||
singleViewPlatform.MainView = new MainView |
|||
{ |
|||
DataContext = new MainViewModel() |
|||
}; |
|||
} |
|||
|
|||
base.OnFrameworkInitializationCompleted(); |
|||
} |
|||
} |
|||
} |
|||
|
After Width: | Height: | Size: 172 KiB |
@ -0,0 +1,27 @@ |
|||
<Project Sdk="Microsoft.NET.Sdk"> |
|||
<PropertyGroup> |
|||
<TargetFramework>net7.0</TargetFramework> |
|||
<Nullable>enable</Nullable> |
|||
<LangVersion>latest</LangVersion> |
|||
<AvaloniaUseCompiledBindingsByDefault>true</AvaloniaUseCompiledBindingsByDefault> |
|||
</PropertyGroup> |
|||
|
|||
|
|||
<ItemGroup> |
|||
<Compile Update="**\*.xaml.cs"> |
|||
<DependentUpon>%(Filename)</DependentUpon> |
|||
</Compile> |
|||
<AvaloniaResource Include="**\*.xaml"> |
|||
<SubType>Designer</SubType> |
|||
</AvaloniaResource> |
|||
<AvaloniaResource Include="Assets\**" /> |
|||
</ItemGroup> |
|||
|
|||
<ItemGroup> |
|||
<ProjectReference Include="..\..\packages\Avalonia\Avalonia.csproj" /> |
|||
<ProjectReference Include="..\..\src\Avalonia.Themes.Fluent\Avalonia.Themes.Fluent.csproj" /> |
|||
<ProjectReference Include="..\MiniMvvm\MiniMvvm.csproj" /> |
|||
</ItemGroup> |
|||
|
|||
<Import Project="..\..\build\BuildTargets.targets" /> |
|||
</Project> |
|||
@ -0,0 +1,31 @@ |
|||
using System; |
|||
using Avalonia.Controls; |
|||
using Avalonia.Controls.Templates; |
|||
using MiniMvvm; |
|||
|
|||
namespace SafeAreaDemo |
|||
{ |
|||
public class ViewLocator : IDataTemplate |
|||
{ |
|||
public Control? Build(object? data) |
|||
{ |
|||
if (data is null) |
|||
return null; |
|||
|
|||
var name = data.GetType().FullName!.Replace("ViewModel", "View"); |
|||
var type = Type.GetType(name); |
|||
|
|||
if (type != null) |
|||
{ |
|||
return (Control)Activator.CreateInstance(type)!; |
|||
} |
|||
|
|||
return new TextBlock { Text = name }; |
|||
} |
|||
|
|||
public bool Match(object? data) |
|||
{ |
|||
return data is ViewModelBase; |
|||
} |
|||
} |
|||
} |
|||
@ -0,0 +1,112 @@ |
|||
using Avalonia; |
|||
using Avalonia.Controls.Platform; |
|||
using MiniMvvm; |
|||
|
|||
namespace SafeAreaDemo.ViewModels |
|||
{ |
|||
public class MainViewModel : ViewModelBase |
|||
{ |
|||
private bool _useSafeArea = true; |
|||
private bool _fullscreen; |
|||
private IInsetsManager? _insetsManager; |
|||
private bool _hideSystemBars; |
|||
|
|||
public Thickness SafeAreaPadding |
|||
{ |
|||
get |
|||
{ |
|||
return _insetsManager?.SafeAreaPadding ?? default; |
|||
} |
|||
} |
|||
|
|||
public Thickness ViewPadding |
|||
{ |
|||
get |
|||
{ |
|||
return _useSafeArea ? SafeAreaPadding : default; |
|||
} |
|||
} |
|||
|
|||
public bool UseSafeArea |
|||
{ |
|||
get => _useSafeArea; |
|||
set |
|||
{ |
|||
_useSafeArea = value; |
|||
|
|||
this.RaisePropertyChanged(); |
|||
|
|||
RaiseSafeAreaChanged(); |
|||
} |
|||
} |
|||
|
|||
public bool Fullscreen |
|||
{ |
|||
get => _fullscreen; |
|||
set |
|||
{ |
|||
_fullscreen = value; |
|||
|
|||
if (_insetsManager != null) |
|||
{ |
|||
_insetsManager.DisplayEdgeToEdge = value; |
|||
} |
|||
|
|||
this.RaisePropertyChanged(); |
|||
|
|||
RaiseSafeAreaChanged(); |
|||
} |
|||
} |
|||
|
|||
public bool HideSystemBars |
|||
{ |
|||
get => _hideSystemBars; |
|||
set |
|||
{ |
|||
_hideSystemBars = value; |
|||
|
|||
if (_insetsManager != null) |
|||
{ |
|||
_insetsManager.IsSystemBarVisible = !value; |
|||
} |
|||
|
|||
this.RaisePropertyChanged(); |
|||
|
|||
RaiseSafeAreaChanged(); |
|||
} |
|||
} |
|||
|
|||
internal IInsetsManager? InsetsManager |
|||
{ |
|||
get => _insetsManager; |
|||
set |
|||
{ |
|||
if (_insetsManager != null) |
|||
{ |
|||
_insetsManager.SafeAreaChanged -= InsetsManager_SafeAreaChanged; |
|||
} |
|||
|
|||
_insetsManager = value; |
|||
|
|||
if (_insetsManager != null) |
|||
{ |
|||
_insetsManager.SafeAreaChanged += InsetsManager_SafeAreaChanged; |
|||
|
|||
_insetsManager.DisplayEdgeToEdge = _fullscreen; |
|||
_insetsManager.IsSystemBarVisible = !_hideSystemBars; |
|||
} |
|||
} |
|||
} |
|||
|
|||
private void InsetsManager_SafeAreaChanged(object? sender, SafeAreaChangedArgs e) |
|||
{ |
|||
RaiseSafeAreaChanged(); |
|||
} |
|||
|
|||
private void RaiseSafeAreaChanged() |
|||
{ |
|||
this.RaisePropertyChanged(nameof(SafeAreaPadding)); |
|||
this.RaisePropertyChanged(nameof(ViewPadding)); |
|||
} |
|||
} |
|||
} |
|||
@ -0,0 +1,52 @@ |
|||
<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="clr-namespace:SafeAreaDemo.ViewModels" |
|||
mc:Ignorable="d" |
|||
d:DesignWidth="800" |
|||
d:DesignHeight="450" |
|||
x:Class="SafeAreaDemo.Views.MainView" |
|||
x:DataType="vm:MainViewModel"> |
|||
<Grid HorizontalAlignment="Stretch" |
|||
VerticalAlignment="Stretch"> |
|||
<Border BorderBrush="Red" |
|||
Margin="{Binding ViewPadding}" |
|||
BorderThickness="1"> |
|||
<Grid> |
|||
<Label Margin="5" |
|||
Foreground="Red" |
|||
HorizontalAlignment="Stretch" |
|||
HorizontalContentAlignment="Right">View Bounds</Label> |
|||
<Label Margin="5" |
|||
Foreground="Red" |
|||
VerticalAlignment="Bottom" |
|||
HorizontalContentAlignment="Right">View Bounds</Label> |
|||
</Grid> |
|||
</Border> |
|||
<Border BorderBrush="LimeGreen" |
|||
Margin="{Binding SafeAreaPadding}" |
|||
BorderThickness="1"> |
|||
<DockPanel> |
|||
<Label Margin="5" |
|||
Foreground="LimeGreen" |
|||
DockPanel.Dock="Bottom" |
|||
HorizontalAlignment="Stretch" |
|||
HorizontalContentAlignment="Left" >Safe Area</Label> |
|||
<Grid DockPanel.Dock="Bottom" |
|||
HorizontalAlignment="Stretch" |
|||
VerticalAlignment="Stretch"> |
|||
<StackPanel Orientation="Vertical" |
|||
HorizontalAlignment="Center" |
|||
VerticalAlignment="Center"> |
|||
<Label HorizontalAlignment="Left">Options:</Label> |
|||
<CheckBox IsChecked="{Binding Fullscreen}">Fullscreen</CheckBox> |
|||
<CheckBox IsChecked="{Binding UseSafeArea}">Use Safe Area</CheckBox> |
|||
<CheckBox IsChecked="{Binding HideSystemBars}">Hide System Bars</CheckBox> |
|||
<TextBox Width="200" Watermark="Tap to Show Keyboard"/> |
|||
</StackPanel> |
|||
</Grid> |
|||
</DockPanel> |
|||
</Border> |
|||
</Grid> |
|||
</UserControl> |
|||
@ -0,0 +1,25 @@ |
|||
using Avalonia.Controls; |
|||
using Avalonia.Markup.Xaml; |
|||
using SafeAreaDemo.ViewModels; |
|||
|
|||
namespace SafeAreaDemo.Views |
|||
{ |
|||
public partial class MainView : UserControl |
|||
{ |
|||
public MainView() |
|||
{ |
|||
AvaloniaXamlLoader.Load(this); |
|||
} |
|||
|
|||
protected override void OnLoaded() |
|||
{ |
|||
base.OnLoaded(); |
|||
|
|||
var insetsManager = TopLevel.GetTopLevel(this)?.InsetsManager; |
|||
if (insetsManager != null && DataContext is MainViewModel viewModel) |
|||
{ |
|||
viewModel.InsetsManager = insetsManager; |
|||
} |
|||
} |
|||
} |
|||
} |
|||
@ -0,0 +1,12 @@ |
|||
<Window xmlns="https://github.com/avaloniaui" |
|||
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" |
|||
xmlns:vm="using:SafeAreaDemo.ViewModels" |
|||
xmlns:d="http://schemas.microsoft.com/expression/blend/2008" |
|||
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" |
|||
xmlns:views="clr-namespace:SafeAreaDemo.Views" |
|||
mc:Ignorable="d" d:DesignWidth="800" d:DesignHeight="450" |
|||
x:Class="SafeAreaDemo.Views.MainWindow" |
|||
Icon="/Assets/avalonia-logo.ico" |
|||
Title="SafeAreaDemo"> |
|||
<views:MainView /> |
|||
</Window> |
|||
@ -0,0 +1,13 @@ |
|||
using Avalonia.Controls; |
|||
using Avalonia.Markup.Xaml; |
|||
|
|||
namespace SafeAreaDemo.Views |
|||
{ |
|||
public partial class MainWindow : Window |
|||
{ |
|||
public MainWindow() |
|||
{ |
|||
AvaloniaXamlLoader.Load(this); |
|||
} |
|||
} |
|||
} |
|||
@ -0,0 +1,14 @@ |
|||
<Application xmlns="https://github.com/avaloniaui" |
|||
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" |
|||
x:Class="VirtualizationDemo.App"> |
|||
<Application.Styles> |
|||
<FluentTheme/> |
|||
</Application.Styles> |
|||
<Application.Resources> |
|||
<ResourceDictionary> |
|||
<ResourceDictionary.MergedDictionaries> |
|||
<ResourceInclude Source="avares://ControlSamples/HamburgerMenu/HamburgerMenu.xaml" /> |
|||
</ResourceDictionary.MergedDictionaries> |
|||
</ResourceDictionary> |
|||
</Application.Resources> |
|||
</Application> |
|||
@ -0,0 +1,20 @@ |
|||
using Avalonia; |
|||
using Avalonia.Controls.ApplicationLifetimes; |
|||
using Avalonia.Markup.Xaml; |
|||
|
|||
namespace VirtualizationDemo; |
|||
|
|||
public partial class App : Application |
|||
{ |
|||
public override void Initialize() |
|||
{ |
|||
AvaloniaXamlLoader.Load(this); |
|||
} |
|||
|
|||
public override void OnFrameworkInitializationCompleted() |
|||
{ |
|||
if (ApplicationLifetime is IClassicDesktopStyleApplicationLifetime desktop) |
|||
desktop.MainWindow = new MainWindow(); |
|||
base.OnFrameworkInitializationCompleted(); |
|||
} |
|||
} |
|||
@ -1,7 +0,0 @@ |
|||
<Application xmlns="https://github.com/avaloniaui" |
|||
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" |
|||
x:Class="VirtualizationDemo.App"> |
|||
<Application.Styles> |
|||
<SimpleTheme /> |
|||
</Application.Styles> |
|||
</Application> |
|||
@ -1,21 +0,0 @@ |
|||
using Avalonia; |
|||
using Avalonia.Controls.ApplicationLifetimes; |
|||
using Avalonia.Markup.Xaml; |
|||
|
|||
namespace VirtualizationDemo |
|||
{ |
|||
public class App : Application |
|||
{ |
|||
public override void Initialize() |
|||
{ |
|||
AvaloniaXamlLoader.Load(this); |
|||
} |
|||
|
|||
public override void OnFrameworkInitializationCompleted() |
|||
{ |
|||
if (ApplicationLifetime is IClassicDesktopStyleApplicationLifetime desktop) |
|||
desktop.MainWindow = new MainWindow(); |
|||
base.OnFrameworkInitializationCompleted(); |
|||
} |
|||
} |
|||
} |
|||
@ -0,0 +1,190 @@ |
|||
{ |
|||
"chat": [ |
|||
{ |
|||
"sender": "Alice", |
|||
"message": "Hey Bob! How was your weekend?", |
|||
"timestamp": "2023-04-01T10:00:00" |
|||
}, |
|||
{ |
|||
"sender": "Bob", |
|||
"message": "It was great, thanks for asking. I went on a camping trip with some friends. How about you?", |
|||
"timestamp": "2023-04-01T10:01:00" |
|||
}, |
|||
{ |
|||
"sender": "Alice", |
|||
"message": "My weekend was pretty chill. I just stayed home and caught up on some TV shows.", |
|||
"timestamp": "2023-04-01T10:03:00" |
|||
}, |
|||
{ |
|||
"sender": "Bob", |
|||
"message": "That sounds relaxing. What shows did you watch?", |
|||
"timestamp": "2023-04-01T10:05:00" |
|||
}, |
|||
{ |
|||
"sender": "Alice", |
|||
"message": "I watched the new season of 'Stranger Things' and started watching 'Ozark'. Have you seen them?", |
|||
"timestamp": "2023-04-01T10:07:00" |
|||
}, |
|||
{ |
|||
"sender": "Bob", |
|||
"message": "Yeah, I've seen both of those. They're really good! What do you think of them so far?", |
|||
"timestamp": "2023-04-01T10:10:00" |
|||
}, |
|||
{ |
|||
"sender": "Alice", |
|||
"message": "I'm really enjoying 'Stranger Things', but 'Ozark' is a bit darker than I expected. I'm only a few episodes in though, so we'll see how it goes.", |
|||
"timestamp": "2023-04-01T10:12:00" |
|||
}, |
|||
{ |
|||
"sender": "Bob", |
|||
"message": "Yeah, 'Ozark' can be intense at times, but it's really well done. Keep watching, it gets even better.", |
|||
"timestamp": "2023-04-01T10:15:00" |
|||
}, |
|||
{ |
|||
"sender": "Alice", |
|||
"message": "Thanks for the recommendation, I'll definitely keep watching. So, how's work been for you lately?", |
|||
"timestamp": "2023-04-01T10:20:00" |
|||
}, |
|||
{ |
|||
"sender": "Bob", |
|||
"message": "It's been pretty busy, but I'm managing. How about you?", |
|||
"timestamp": "2023-04-01T10:22:00" |
|||
}, |
|||
{ |
|||
"sender": "Alice", |
|||
"message": "Same here, things have been pretty hectic. But it keeps us on our toes, right?", |
|||
"timestamp": "2023-04-01T10:25:00" |
|||
}, |
|||
{ |
|||
"sender": "Bob", |
|||
"message": "Absolutely. Hey, have you heard about the new project we're starting next week?", |
|||
"timestamp": "2023-04-01T10:30:00" |
|||
}, |
|||
{ |
|||
"sender": "Alice", |
|||
"message": "No, I haven't. What's it about?", |
|||
"timestamp": "2023-04-01T10:32:00" |
|||
}, |
|||
{ |
|||
"sender": "Bob", |
|||
"message": "It's a big project for a new client, and it's going to require a lot of extra hours from all of us. But the pay is going to be great,so it's definitely worth the extra effort. I'll fill you in on the details later, but for now, let's just enjoy our coffee break, shall we?", |
|||
"timestamp": "2023-04-01T10:35:00" |
|||
}, |
|||
{ |
|||
"sender": "Alice", |
|||
"message": "Sounds good to me. I could use a break right about now.", |
|||
"timestamp": "2023-04-01T10:40:00" |
|||
}, |
|||
{ |
|||
"sender": "Bob", |
|||
"message": "Me too. So, have you tried the new caf� down the street yet?", |
|||
"timestamp": "2023-04-01T10:45:00" |
|||
}, |
|||
{ |
|||
"sender": "Alice", |
|||
"message": "No, I haven't. Is it any good?", |
|||
"timestamp": "2023-04-01T10:47:00" |
|||
}, |
|||
{ |
|||
"sender": "Bob", |
|||
"message": "It's really good! They have the best croissants I've ever tasted.", |
|||
"timestamp": "2023-04-01T10:50:00" |
|||
}, |
|||
{ |
|||
"sender": "Alice", |
|||
"message": "Hmm, I'll have to try it out sometime. Do they have any vegan options?", |
|||
"timestamp": "2023-04-01T10:52:00" |
|||
}, |
|||
{ |
|||
"sender": "Bob", |
|||
"message": "I'm not sure, but I think they do. You should ask them the next time you go there.", |
|||
"timestamp": "2023-04-01T10:55:00" |
|||
}, |
|||
{ |
|||
"sender": "Alice", |
|||
"message": "Thanks for the suggestion. I'm always looking for good vegan options around here.", |
|||
"timestamp": "2023-04-01T11:00:00" |
|||
}, |
|||
{ |
|||
"sender": "Bob", |
|||
"message": "No problem. So, have you made any plans for the weekend yet?", |
|||
"timestamp": "2023-04-01T11:05:00" |
|||
}, |
|||
{ |
|||
"sender": "Alice", |
|||
"message": "Not yet. I was thinking of maybe going for a hike or something. What about you?", |
|||
"timestamp": "2023-04-01T11:07:00" |
|||
}, |
|||
{ |
|||
"sender": "Bob", |
|||
"message": "I haven't made any plans either. Maybe we could do something together?", |
|||
"timestamp": "2023-04-01T11:10:00" |
|||
}, |
|||
{ |
|||
"sender": "Alice", |
|||
"message": "That sounds like a great idea! Let's plan on it.", |
|||
"timestamp": "2023-04-01T11:12:00" |
|||
}, |
|||
{ |
|||
"sender": "Bob", |
|||
"message": "Awesome. I'll check out some hiking trails and let you know which ones look good.", |
|||
"timestamp": "2023-04-01T11:15:00" |
|||
}, |
|||
{ |
|||
"sender": "Alice", |
|||
"message": "Sounds good. I can't wait!", |
|||
"timestamp": "2023-04-01T11:20:00" |
|||
}, |
|||
{ |
|||
"sender": "John", |
|||
"message": "Hey Lisa, how was your day?", |
|||
"timestamp": "2023-04-01T18:00:00" |
|||
}, |
|||
{ |
|||
"sender": "Lisa", |
|||
"message": "It was good, thanks for asking. How about you?", |
|||
"timestamp": "2023-04-01T18:05:00" |
|||
}, |
|||
{ |
|||
"sender": "John", |
|||
"message": "Eh, it was alright. Work was pretty busy, but nothing too crazy.", |
|||
"timestamp": "2023-04-01T18:10:00" |
|||
}, |
|||
{ |
|||
"sender": "Lisa", |
|||
"message": "Yeah, I know what you mean. My boss has been on my case lately about meeting our deadlines.", |
|||
"timestamp": "2023-04-01T18:15:00" |
|||
}, |
|||
{ |
|||
"sender": "John", |
|||
"message": "That sucks. Are you feeling stressed out?", |
|||
"timestamp": "2023-04-01T18:20:00" |
|||
}, |
|||
{ |
|||
"sender": "Lisa", |
|||
"message": "A little bit, yeah. But I'm trying to stay positive and focus on getting my work done.", |
|||
"timestamp": "2023-04-01T18:25:00" |
|||
}, |
|||
{ |
|||
"sender": "John", |
|||
"message": "That's a good attitude to have. Have you tried doing some meditation or other relaxation techniques?", |
|||
"timestamp": "2023-04-01T18:30:00" |
|||
}, |
|||
{ |
|||
"sender": "Lisa", |
|||
"message": "I haven't, but I've been thinking about it. Do you have any suggestions?", |
|||
"timestamp": "2023-04-01T18:35:00" |
|||
}, |
|||
{ |
|||
"sender": "John", |
|||
"message": "Sure, I could send you some links to guided meditations that I've found helpful. And there are also some great apps out there that can help you with relaxation.", |
|||
"timestamp": "2023-04-01T18:40:00" |
|||
}, |
|||
{ |
|||
"sender": "Lisa", |
|||
"message": "That would be awesome, thanks so much!", |
|||
"timestamp": "2023-04-01T18:45:00" |
|||
} |
|||
] |
|||
} |
|||
|
|||
@ -0,0 +1,20 @@ |
|||
<Window xmlns="https://github.com/avaloniaui" |
|||
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" |
|||
xmlns:controls="using:ControlSamples" |
|||
xmlns:vm="using:VirtualizationDemo.ViewModels" |
|||
xmlns:views="using:VirtualizationDemo.Views" |
|||
x:Class="VirtualizationDemo.MainWindow" |
|||
Title="AvaloniaUI Virtualization Demo" |
|||
x:DataType="vm:MainWindowViewModel"> |
|||
<controls:HamburgerMenu> |
|||
<TabItem Header="Playground" ScrollViewer.VerticalScrollBarVisibility="Disabled"> |
|||
<views:PlaygroundPageView DataContext="{Binding Playground}"/> |
|||
</TabItem> |
|||
<TabItem Header="Chat" ScrollViewer.VerticalScrollBarVisibility="Disabled"> |
|||
<views:ChatPageView DataContext="{Binding Chat}"/> |
|||
</TabItem> |
|||
<TabItem Header="Expanders" ScrollViewer.VerticalScrollBarVisibility="Disabled"> |
|||
<views:ExpanderPageView DataContext="{Binding Expanders}"/> |
|||
</TabItem> |
|||
</controls:HamburgerMenu> |
|||
</Window> |
|||
@ -0,0 +1,15 @@ |
|||
using Avalonia; |
|||
using Avalonia.Controls; |
|||
using VirtualizationDemo.ViewModels; |
|||
|
|||
namespace VirtualizationDemo; |
|||
|
|||
public partial class MainWindow : Window |
|||
{ |
|||
public MainWindow() |
|||
{ |
|||
InitializeComponent(); |
|||
this.AttachDevTools(); |
|||
DataContext = new MainWindowViewModel(); |
|||
} |
|||
} |
|||
@ -1,64 +0,0 @@ |
|||
<Window xmlns="https://github.com/avaloniaui" |
|||
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" |
|||
xmlns:viewModels="using:VirtualizationDemo.ViewModels" |
|||
x:Class="VirtualizationDemo.MainWindow" |
|||
Title="AvaloniaUI Virtualization Test" |
|||
Width="800" |
|||
Height="600" |
|||
x:DataType="viewModels:MainWindowViewModel"> |
|||
<DockPanel LastChildFill="True" Margin="16"> |
|||
<StackPanel DockPanel.Dock="Right" |
|||
Margin="16 0 0 0" |
|||
Width="150" |
|||
Spacing="4"> |
|||
<ComboBox 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); |
|||
} |
|||
} |
|||
} |
|||
@ -0,0 +1,23 @@ |
|||
using System; |
|||
using System.IO; |
|||
using System.Text.Json; |
|||
|
|||
namespace VirtualizationDemo.Models; |
|||
|
|||
public class ChatFile |
|||
{ |
|||
public ChatMessage[]? Chat { get; set; } |
|||
|
|||
public static ChatFile Load(string path) |
|||
{ |
|||
var options = new JsonSerializerOptions |
|||
{ |
|||
PropertyNameCaseInsensitive = true |
|||
}; |
|||
|
|||
using var s = File.OpenRead(path); |
|||
return JsonSerializer.Deserialize<ChatFile>(s, options)!; |
|||
} |
|||
} |
|||
|
|||
public record ChatMessage(string Sender, string Message, DateTimeOffset Timestamp); |
|||
@ -1,15 +1,14 @@ |
|||
using Avalonia; |
|||
|
|||
namespace VirtualizationDemo |
|||
namespace VirtualizationDemo; |
|||
|
|||
class Program |
|||
{ |
|||
class Program |
|||
{ |
|||
public static AppBuilder BuildAvaloniaApp() |
|||
=> AppBuilder.Configure<App>() |
|||
.UsePlatformDetect() |
|||
.LogToTrace(); |
|||
public static AppBuilder BuildAvaloniaApp() |
|||
=> AppBuilder.Configure<App>() |
|||
.UsePlatformDetect() |
|||
.LogToTrace(); |
|||
|
|||
public static int Main(string[] args) |
|||
=> BuildAvaloniaApp().StartWithClassicDesktopLifetime(args); |
|||
} |
|||
public static int Main(string[] args) |
|||
=> BuildAvaloniaApp().StartWithClassicDesktopLifetime(args); |
|||
} |
|||
|
|||
@ -0,0 +1,17 @@ |
|||
using System; |
|||
using System.Collections.ObjectModel; |
|||
using System.IO; |
|||
using VirtualizationDemo.Models; |
|||
|
|||
namespace VirtualizationDemo.ViewModels; |
|||
|
|||
public class ChatPageViewModel |
|||
{ |
|||
public ChatPageViewModel() |
|||
{ |
|||
var chat = ChatFile.Load(Path.Combine("Assets", "chat.json")); |
|||
Messages = new(chat.Chat ?? Array.Empty<ChatMessage>()); |
|||
} |
|||
|
|||
public ObservableCollection<ChatMessage> Messages { get; } |
|||
} |
|||
@ -0,0 +1,21 @@ |
|||
using MiniMvvm; |
|||
|
|||
namespace VirtualizationDemo.ViewModels; |
|||
|
|||
public class ExpanderItemViewModel : ViewModelBase |
|||
{ |
|||
private string? _header; |
|||
private bool _isExpanded; |
|||
|
|||
public string? Header |
|||
{ |
|||
get => _header; |
|||
set => RaiseAndSetIfChanged(ref _header, value); |
|||
} |
|||
|
|||
public bool IsExpanded |
|||
{ |
|||
get => _isExpanded; |
|||
set => RaiseAndSetIfChanged(ref _isExpanded, value); |
|||
} |
|||
} |
|||
@ -0,0 +1,17 @@ |
|||
using System.Collections.ObjectModel; |
|||
using System.Linq; |
|||
|
|||
namespace VirtualizationDemo.ViewModels; |
|||
|
|||
internal class ExpanderPageViewModel |
|||
{ |
|||
public ExpanderPageViewModel() |
|||
{ |
|||
Items = new(Enumerable.Range(0, 100).Select(x => new ExpanderItemViewModel |
|||
{ |
|||
Header = $"Item {x}", |
|||
})); |
|||
} |
|||
|
|||
public ObservableCollection<ExpanderItemViewModel> Items { get; set; } |
|||
} |
|||
@ -1,26 +0,0 @@ |
|||
using System; |
|||
using MiniMvvm; |
|||
|
|||
namespace VirtualizationDemo.ViewModels |
|||
{ |
|||
internal class ItemViewModel : ViewModelBase |
|||
{ |
|||
private string _prefix; |
|||
private int _index; |
|||
private double _height = double.NaN; |
|||
|
|||
public ItemViewModel(int index, string prefix = "Item") |
|||
{ |
|||
_prefix = prefix; |
|||
_index = index; |
|||
} |
|||
|
|||
public string Header => $"{_prefix} {_index}"; |
|||
|
|||
public double Height |
|||
{ |
|||
get => _height; |
|||
set => this.RaiseAndSetIfChanged(ref _height, value); |
|||
} |
|||
} |
|||
} |
|||
@ -1,160 +1,10 @@ |
|||
using System; |
|||
using System.Collections.Generic; |
|||
using System.Linq; |
|||
using System.Reactive; |
|||
using Avalonia.Collections; |
|||
using Avalonia.Controls; |
|||
using Avalonia.Controls.Primitives; |
|||
using Avalonia.Layout; |
|||
using Avalonia.Controls.Selection; |
|||
using MiniMvvm; |
|||
using MiniMvvm; |
|||
|
|||
namespace VirtualizationDemo.ViewModels |
|||
{ |
|||
internal class MainWindowViewModel : ViewModelBase |
|||
{ |
|||
private int _itemCount = 200; |
|||
private string _newItemString = "New Item"; |
|||
private int _newItemIndex; |
|||
private AvaloniaList<ItemViewModel> _items; |
|||
private string _prefix = "Item"; |
|||
private ScrollBarVisibility _horizontalScrollBarVisibility = ScrollBarVisibility.Auto; |
|||
private ScrollBarVisibility _verticalScrollBarVisibility = ScrollBarVisibility.Auto; |
|||
private Orientation _orientation = Orientation.Vertical; |
|||
|
|||
public MainWindowViewModel() |
|||
{ |
|||
this.WhenAnyValue(x => x.ItemCount).Subscribe(ResizeItems); |
|||
RecreateCommand = MiniCommand.Create(() => Recreate()); |
|||
|
|||
AddItemCommand = MiniCommand.Create(() => AddItem()); |
|||
|
|||
RemoveItemCommand = MiniCommand.Create(() => Remove()); |
|||
|
|||
SelectFirstCommand = MiniCommand.Create(() => SelectItem(0)); |
|||
|
|||
SelectLastCommand = MiniCommand.Create(() => SelectItem(Items.Count - 1)); |
|||
} |
|||
|
|||
public string NewItemString |
|||
{ |
|||
get { return _newItemString; } |
|||
set { this.RaiseAndSetIfChanged(ref _newItemString, value); } |
|||
} |
|||
|
|||
public int ItemCount |
|||
{ |
|||
get { return _itemCount; } |
|||
set { this.RaiseAndSetIfChanged(ref _itemCount, value); } |
|||
} |
|||
|
|||
public SelectionModel<ItemViewModel> Selection { get; } = new SelectionModel<ItemViewModel>(); |
|||
|
|||
public AvaloniaList<ItemViewModel> Items |
|||
{ |
|||
get { return _items; } |
|||
private set { this.RaiseAndSetIfChanged(ref _items, value); } |
|||
} |
|||
|
|||
public Orientation Orientation |
|||
{ |
|||
get { return _orientation; } |
|||
set { this.RaiseAndSetIfChanged(ref _orientation, value); } |
|||
} |
|||
|
|||
public IEnumerable<Orientation> Orientations => |
|||
Enum.GetValues(typeof(Orientation)).Cast<Orientation>(); |
|||
|
|||
public ScrollBarVisibility HorizontalScrollBarVisibility |
|||
{ |
|||
get { return _horizontalScrollBarVisibility; } |
|||
set { this.RaiseAndSetIfChanged(ref _horizontalScrollBarVisibility, value); } |
|||
} |
|||
namespace VirtualizationDemo.ViewModels; |
|||
|
|||
public ScrollBarVisibility VerticalScrollBarVisibility |
|||
{ |
|||
get { return _verticalScrollBarVisibility; } |
|||
set { this.RaiseAndSetIfChanged(ref _verticalScrollBarVisibility, value); } |
|||
} |
|||
|
|||
public IEnumerable<ScrollBarVisibility> ScrollBarVisibilities => |
|||
Enum.GetValues(typeof(ScrollBarVisibility)).Cast<ScrollBarVisibility>(); |
|||
|
|||
public MiniCommand AddItemCommand { get; private set; } |
|||
public MiniCommand RecreateCommand { get; private set; } |
|||
public MiniCommand RemoveItemCommand { get; private set; } |
|||
public MiniCommand SelectFirstCommand { get; private set; } |
|||
public MiniCommand SelectLastCommand { get; private set; } |
|||
|
|||
public void RandomizeSize() |
|||
{ |
|||
var random = new Random(); |
|||
|
|||
foreach (var i in Items) |
|||
{ |
|||
i.Height = random.Next(240) + 10; |
|||
} |
|||
} |
|||
|
|||
public void ResetSize() |
|||
{ |
|||
foreach (var i in Items) |
|||
{ |
|||
i.Height = double.NaN; |
|||
} |
|||
} |
|||
|
|||
private void ResizeItems(int count) |
|||
{ |
|||
if (Items == null) |
|||
{ |
|||
var items = Enumerable.Range(0, count) |
|||
.Select(x => new ItemViewModel(x)); |
|||
Items = new AvaloniaList<ItemViewModel>(items); |
|||
} |
|||
else if (count > Items.Count) |
|||
{ |
|||
var items = Enumerable.Range(Items.Count, count - Items.Count) |
|||
.Select(x => new ItemViewModel(x)); |
|||
Items.AddRange(items); |
|||
} |
|||
else if (count < Items.Count) |
|||
{ |
|||
Items.RemoveRange(count, Items.Count - count); |
|||
} |
|||
} |
|||
|
|||
private void AddItem() |
|||
{ |
|||
var index = Items.Count; |
|||
|
|||
if (Selection.SelectedItems.Count > 0) |
|||
{ |
|||
index = Selection.SelectedIndex; |
|||
} |
|||
|
|||
Items.Insert(index, new ItemViewModel(_newItemIndex++, NewItemString)); |
|||
} |
|||
|
|||
private void Remove() |
|||
{ |
|||
if (Selection.SelectedItems.Count > 0) |
|||
{ |
|||
Items.RemoveAll(Selection.SelectedItems.ToList()); |
|||
} |
|||
} |
|||
|
|||
private void Recreate() |
|||
{ |
|||
_prefix = _prefix == "Item" ? "Recreated" : "Item"; |
|||
var items = Enumerable.Range(0, _itemCount) |
|||
.Select(x => new ItemViewModel(x, _prefix)); |
|||
Items = new AvaloniaList<ItemViewModel>(items); |
|||
} |
|||
|
|||
private void SelectItem(int index) |
|||
{ |
|||
Selection.SelectedIndex = index; |
|||
} |
|||
} |
|||
internal class MainWindowViewModel : ViewModelBase |
|||
{ |
|||
public PlaygroundPageViewModel Playground { get; } = new(); |
|||
public ChatPageViewModel Chat { get; } = new(); |
|||
public ExpanderPageViewModel Expanders { get; } = new(); |
|||
} |
|||
|
|||
@ -0,0 +1,17 @@ |
|||
using MiniMvvm; |
|||
|
|||
namespace VirtualizationDemo.ViewModels; |
|||
|
|||
public class PlaygroundItemViewModel : ViewModelBase |
|||
{ |
|||
private string? _header; |
|||
|
|||
public PlaygroundItemViewModel(int index) => Header = $"Item {index}"; |
|||
public PlaygroundItemViewModel(string? header) => Header = header; |
|||
|
|||
public string? Header |
|||
{ |
|||
get => _header; |
|||
set => RaiseAndSetIfChanged(ref _header, value); |
|||
} |
|||
} |
|||
@ -0,0 +1,95 @@ |
|||
using System; |
|||
using System.Collections.ObjectModel; |
|||
using System.Linq; |
|||
using Avalonia; |
|||
using Avalonia.Controls; |
|||
using Avalonia.Controls.Selection; |
|||
using MiniMvvm; |
|||
|
|||
namespace VirtualizationDemo.ViewModels; |
|||
|
|||
public class PlaygroundPageViewModel : ViewModelBase |
|||
{ |
|||
private SelectionMode _selectionMode = SelectionMode.Multiple; |
|||
private int _scrollToIndex = 500; |
|||
private string? _newItemHeader = "New Item 1"; |
|||
|
|||
public PlaygroundPageViewModel() |
|||
{ |
|||
Items = new(Enumerable.Range(0, 1000).Select(x => new PlaygroundItemViewModel(x))); |
|||
Selection = new(); |
|||
} |
|||
|
|||
public ObservableCollection<PlaygroundItemViewModel> Items { get; } |
|||
|
|||
public bool Multiple |
|||
{ |
|||
get => _selectionMode.HasAnyFlag(SelectionMode.Multiple); |
|||
set => SetSelectionMode(SelectionMode.Multiple, value); |
|||
} |
|||
|
|||
public bool Toggle |
|||
{ |
|||
get => _selectionMode.HasAnyFlag(SelectionMode.Toggle); |
|||
set => SetSelectionMode(SelectionMode.Toggle, value); |
|||
} |
|||
|
|||
public bool AlwaysSelected |
|||
{ |
|||
get => _selectionMode.HasAnyFlag(SelectionMode.AlwaysSelected); |
|||
set => SetSelectionMode(SelectionMode.AlwaysSelected, value); |
|||
} |
|||
|
|||
public SelectionModel<PlaygroundItemViewModel> Selection { get; } |
|||
|
|||
public SelectionMode SelectionMode |
|||
{ |
|||
get => _selectionMode; |
|||
set => RaiseAndSetIfChanged(ref _selectionMode, value); |
|||
} |
|||
|
|||
public int ScrollToIndex |
|||
{ |
|||
get => _scrollToIndex; |
|||
set => RaiseAndSetIfChanged(ref _scrollToIndex, value); |
|||
} |
|||
|
|||
public string? NewItemHeader |
|||
{ |
|||
get => _newItemHeader; |
|||
set => RaiseAndSetIfChanged(ref _newItemHeader, value); |
|||
} |
|||
|
|||
public void ExecuteScrollToIndex() |
|||
{ |
|||
Selection.Select(ScrollToIndex); |
|||
} |
|||
|
|||
public void RandomizeScrollToIndex() |
|||
{ |
|||
var rnd = new Random(); |
|||
ScrollToIndex = rnd.Next(Items.Count); |
|||
} |
|||
|
|||
public void AddAtSelectedIndex() |
|||
{ |
|||
if (Selection.SelectedIndex == -1) |
|||
return; |
|||
Items.Insert(Selection.SelectedIndex, new(NewItemHeader)); |
|||
} |
|||
|
|||
public void DeleteSelectedItem() |
|||
{ |
|||
var count = Selection.Count; |
|||
for (var i = count - 1; i >= 0; i--) |
|||
Items.RemoveAt(Selection.SelectedIndexes[i]); |
|||
} |
|||
|
|||
private void SetSelectionMode(SelectionMode mode, bool value) |
|||
{ |
|||
if (value) |
|||
SelectionMode |= mode; |
|||
else |
|||
SelectionMode &= ~mode; |
|||
} |
|||
} |
|||
@ -0,0 +1,39 @@ |
|||
<UserControl xmlns="https://github.com/avaloniaui" |
|||
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" |
|||
xmlns:d="http://schemas.microsoft.com/expression/blend/2008" |
|||
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" |
|||
xmlns:vm="using:VirtualizationDemo.ViewModels" |
|||
mc:Ignorable="d" d:DesignWidth="800" d:DesignHeight="450" |
|||
x:Class="VirtualizationDemo.Views.ChatPageView" |
|||
x:DataType="vm:ChatPageViewModel"> |
|||
<ListBox ItemsSource="{Binding Messages}"> |
|||
<ListBox.ItemContainerTheme> |
|||
<ControlTheme TargetType="ListBoxItem" BasedOn="{StaticResource {x:Type ListBoxItem}}"> |
|||
<Setter Property="Padding" Value="8"/> |
|||
</ControlTheme> |
|||
</ListBox.ItemContainerTheme> |
|||
<ListBox.ItemTemplate> |
|||
<DataTemplate> |
|||
<Border CornerRadius="8" |
|||
Background="{DynamicResource SystemControlBackgroundAltHighBrush}" |
|||
TextElement.Foreground="{DynamicResource SystemControlForegroundBaseHighBrush}" |
|||
Padding="6" |
|||
HorizontalAlignment="Left" |
|||
MaxWidth="280"> |
|||
<DockPanel> |
|||
<TextBlock DockPanel.Dock="Top" |
|||
Text="{Binding Sender}" |
|||
FontWeight="Bold"/> |
|||
<TextBlock DockPanel.Dock="Bottom" |
|||
Text="{Binding Timestamp}" |
|||
FontSize="10" |
|||
Foreground="{DynamicResource SystemControlForegroundBaseMediumBrush}" |
|||
TextAlignment="Right" |
|||
Margin="0 4 0 0"/> |
|||
<TextBlock Text="{Binding Message}" TextWrapping="Wrap"/> |
|||
</DockPanel> |
|||
</Border> |
|||
</DataTemplate> |
|||
</ListBox.ItemTemplate> |
|||
</ListBox> |
|||
</UserControl> |
|||
@ -0,0 +1,11 @@ |
|||
using Avalonia.Controls; |
|||
|
|||
namespace VirtualizationDemo.Views; |
|||
|
|||
public partial class ChatPageView : UserControl |
|||
{ |
|||
public ChatPageView() |
|||
{ |
|||
InitializeComponent(); |
|||
} |
|||
} |
|||
@ -0,0 +1,18 @@ |
|||
<UserControl xmlns="https://github.com/avaloniaui" |
|||
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" |
|||
xmlns:d="http://schemas.microsoft.com/expression/blend/2008" |
|||
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" |
|||
xmlns:vm="using:VirtualizationDemo.ViewModels" |
|||
mc:Ignorable="d" d:DesignWidth="800" d:DesignHeight="450" |
|||
x:Class="VirtualizationDemo.Views.ExpanderPageView" |
|||
x:DataType="vm:ExpanderPageViewModel"> |
|||
<ListBox ItemsSource="{Binding Items}"> |
|||
<ListBox.ItemTemplate> |
|||
<DataTemplate> |
|||
<Expander Header="{Binding Header}" IsExpanded="{Binding IsExpanded}"> |
|||
<Border Width="200" Height="300"/> |
|||
</Expander> |
|||
</DataTemplate> |
|||
</ListBox.ItemTemplate> |
|||
</ListBox> |
|||
</UserControl> |
|||
@ -0,0 +1,13 @@ |
|||
using Avalonia; |
|||
using Avalonia.Controls; |
|||
using Avalonia.Markup.Xaml; |
|||
|
|||
namespace VirtualizationDemo.Views; |
|||
|
|||
public partial class ExpanderPageView : UserControl |
|||
{ |
|||
public ExpanderPageView() |
|||
{ |
|||
InitializeComponent(); |
|||
} |
|||
} |
|||
@ -0,0 +1,66 @@ |
|||
<UserControl xmlns="https://github.com/avaloniaui" |
|||
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" |
|||
xmlns:d="http://schemas.microsoft.com/expression/blend/2008" |
|||
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" |
|||
xmlns:vm="using:VirtualizationDemo.ViewModels" |
|||
mc:Ignorable="d" d:DesignWidth="800" d:DesignHeight="450" |
|||
x:Class="VirtualizationDemo.Views.PlaygroundPageView" |
|||
x:DataType="vm:PlaygroundPageViewModel"> |
|||
<DockPanel> |
|||
<StackPanel DockPanel.Dock="Right" Margin="8 0" Width="200"> |
|||
<DropDownButton Content="Selection" HorizontalAlignment="Stretch"> |
|||
<Button.Flyout> |
|||
<Flyout> |
|||
<StackPanel> |
|||
<CheckBox IsChecked="{Binding Multiple}">Multiple</CheckBox> |
|||
<CheckBox IsChecked="{Binding Toggle}">Toggle</CheckBox> |
|||
<CheckBox IsChecked="{Binding AlwaysSelected}">AlwaysSelected</CheckBox> |
|||
<CheckBox IsChecked="{Binding #list.AutoScrollToSelectedItem}">AutoScrollToSelectedItem</CheckBox> |
|||
<CheckBox IsChecked="{Binding #list.WrapSelection}">WrapSelection</CheckBox> |
|||
</StackPanel> |
|||
</Flyout> |
|||
</Button.Flyout> |
|||
</DropDownButton> |
|||
|
|||
<Label>_Select Item</Label> |
|||
<DockPanel> |
|||
<TextBox x:Name="scrollToIndex" Text="{Binding ScrollToIndex}"> |
|||
<TextBox.InnerRightContent> |
|||
<StackPanel Orientation="Horizontal"> |
|||
<Button DockPanel.Dock="Right" |
|||
Command="{Binding RandomizeScrollToIndex}" |
|||
ToolTip.Tip="Randomize"> |
|||
⟳ |
|||
</Button> |
|||
<Button DockPanel.Dock="Right" |
|||
Command="{Binding ExecuteScrollToIndex}" |
|||
ToolTip.Tip="Execute"> |
|||
⮐ |
|||
</Button> |
|||
</StackPanel> |
|||
</TextBox.InnerRightContent> |
|||
</TextBox> |
|||
</DockPanel> |
|||
|
|||
<Label>New Item</Label> |
|||
<TextBox Text="{Binding NewItemHeader}"> |
|||
<TextBox.InnerRightContent> |
|||
<Button Command="{Binding AddAtSelectedIndex}" |
|||
ToolTip.Tip="Add at Selected Index">+</Button> |
|||
</TextBox.InnerRightContent> |
|||
</TextBox> |
|||
|
|||
<Button Command="{Binding DeleteSelectedItem}" Margin="0 8 0 0"> |
|||
Delete Selected |
|||
</Button> |
|||
</StackPanel> |
|||
|
|||
<TextBlock Name="itemCount" DockPanel.Dock="Bottom"/> |
|||
|
|||
<ListBox Name="list" |
|||
ItemsSource="{Binding Items}" |
|||
DisplayMemberBinding="{Binding Header}" |
|||
Selection="{Binding Selection}" |
|||
SelectionMode="{Binding SelectionMode}"/> |
|||
</DockPanel> |
|||
</UserControl> |
|||
@ -0,0 +1,44 @@ |
|||
using System; |
|||
using System.Linq; |
|||
using System.Threading; |
|||
using Avalonia; |
|||
using Avalonia.Controls; |
|||
using Avalonia.Markup.Xaml; |
|||
using Avalonia.Threading; |
|||
|
|||
namespace VirtualizationDemo.Views; |
|||
|
|||
public partial class PlaygroundPageView : UserControl |
|||
{ |
|||
private DispatcherTimer _timer; |
|||
|
|||
public PlaygroundPageView() |
|||
{ |
|||
InitializeComponent(); |
|||
|
|||
_timer = new DispatcherTimer |
|||
{ |
|||
Interval = TimeSpan.FromMilliseconds(500), |
|||
}; |
|||
|
|||
_timer.Tick += TimerTick; |
|||
} |
|||
|
|||
protected override void OnAttachedToVisualTree(VisualTreeAttachmentEventArgs e) |
|||
{ |
|||
base.OnAttachedToVisualTree(e); |
|||
_timer.Start(); |
|||
} |
|||
|
|||
protected override void OnDetachedFromVisualTree(VisualTreeAttachmentEventArgs e) |
|||
{ |
|||
base.OnDetachedFromVisualTree(e); |
|||
_timer.Stop(); |
|||
} |
|||
|
|||
private void TimerTick(object? sender, EventArgs e) |
|||
{ |
|||
var message = $"Realized {list.GetRealizedContainers().Count()} of {list.ItemsPanelRoot?.Children.Count}"; |
|||
itemCount.Text = message; |
|||
} |
|||
} |
|||
@ -1,19 +1,24 @@ |
|||
<Project Sdk="Microsoft.NET.Sdk"> |
|||
<PropertyGroup> |
|||
<OutputType>Exe</OutputType> |
|||
<OutputType>WinExe</OutputType> |
|||
<TargetFramework>net6.0</TargetFramework> |
|||
<IncludeAvaloniaGenerators>true</IncludeAvaloniaGenerators> |
|||
</PropertyGroup> |
|||
<ItemGroup> |
|||
<ProjectReference Include="..\..\packages\Avalonia\Avalonia.csproj" /> |
|||
<ProjectReference Include="..\..\src\Avalonia.Desktop\Avalonia.Desktop.csproj" /> |
|||
<ProjectReference Include="..\..\src\Avalonia.Diagnostics\Avalonia.Diagnostics.csproj" /> |
|||
<ProjectReference Include="..\..\src\Avalonia.Themes.Fluent\Avalonia.Themes.Fluent.csproj" /> |
|||
<ProjectReference Include="..\..\src\Avalonia.Fonts.Inter\Avalonia.Fonts.Inter.csproj" /> |
|||
<ProjectReference Include="..\..\src\Avalonia.Themes.Simple\Avalonia.Themes.Simple.csproj" /> |
|||
<ProjectReference Include="..\..\src\Linux\Avalonia.LinuxFramebuffer\Avalonia.LinuxFramebuffer.csproj" /> |
|||
<ProjectReference Include="..\MiniMvvm\MiniMvvm.csproj" /> |
|||
<ProjectReference Include="..\SampleControls\ControlSamples.csproj" /> |
|||
</ItemGroup> |
|||
<ItemGroup> |
|||
<None Update="Assets\chat.json"> |
|||
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory> |
|||
</None> |
|||
</ItemGroup> |
|||
<Import Project="..\..\build\SampleApp.props" /> |
|||
<Import Project="..\..\build\EmbedXaml.props" /> |
|||
<Import Project="..\..\build\Rx.props" /> |
|||
<Import Condition="'$(TargetFramework)'=='net461'" Project="..\..\build\NetFX.props" /> |
|||
<Import Project="..\..\build\ReferenceCoreLibraries.props" /> |
|||
<Import Project="..\..\build\BuildTargets.targets" /> |
|||
<Import Project="..\..\build\SourceGenerators.props" /> |
|||
<Import Project="..\..\build\NullableEnable.props" /> |
|||
</Project> |
|||
|
|||
@ -0,0 +1,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), Colors.Black); |
|||
|
|||
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 * Math.PI / 180) * ShadowDepth; |
|||
public double OffsetY => Math.Sin(Direction * Math.PI / 180) * ShadowDepth; |
|||
|
|||
public IImmutableEffect ToImmutable() => new ImmutableDropShadowDirectionEffect(OffsetX, OffsetY, BlurRadius, Color, Opacity); |
|||
} |
|||
@ -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; |
|||
} |
|||
} |
|||
@ -0,0 +1,83 @@ |
|||
using System; |
|||
using System.Diagnostics.CodeAnalysis; |
|||
using Avalonia.Animation.Animators; |
|||
using Avalonia.Animation.Easings; |
|||
using Avalonia.Media; |
|||
|
|||
|
|||
// ReSharper disable once CheckNamespace
|
|||
namespace Avalonia.Animation; |
|||
|
|||
/// <summary>
|
|||
/// Transition class that handles <see cref="AvaloniaProperty"/> with <see cref="IEffect"/> type.
|
|||
/// </summary>
|
|||
public class EffectTransition : Transition<IEffect?> |
|||
{ |
|||
private static readonly BlurEffectAnimator s_blurEffectAnimator = new(); |
|||
private static readonly DropShadowEffectAnimator s_dropShadowEffectAnimator = new(); |
|||
private static readonly ImmutableBlurEffect s_DefaultBlur = new ImmutableBlurEffect(0); |
|||
private static readonly ImmutableDropShadowDirectionEffect s_DefaultDropShadow = new(0, 0, 0, default, 0); |
|||
|
|||
bool TryWithAnimator<TAnimator, TInterface>( |
|||
IObservable<double> progress, |
|||
TAnimator animator, |
|||
IEffect? oldValue, IEffect? newValue, TInterface defaultValue, [MaybeNullWhen(false)] out IObservable<IEffect?> observable) |
|||
where TAnimator : EffectAnimatorBase<TInterface> where TInterface : class, IEffect |
|||
{ |
|||
observable = null; |
|||
TInterface? oldI = null, newI = null; |
|||
if (oldValue is TInterface oi) |
|||
{ |
|||
oldI = oi; |
|||
if (newValue is TInterface ni) |
|||
newI = ni; |
|||
else if (newValue == null) |
|||
newI = defaultValue; |
|||
else |
|||
return false; |
|||
} |
|||
else if (newValue is TInterface nv) |
|||
{ |
|||
oldI = defaultValue; |
|||
newI = nv; |
|||
|
|||
} |
|||
else |
|||
return false; |
|||
|
|||
observable = new AnimatorTransitionObservable<IEffect?, Animator<IEffect?>>(animator, progress, Easing, oldI, newI); |
|||
return true; |
|||
|
|||
} |
|||
|
|||
public override IObservable<IEffect?> DoTransition(IObservable<double> progress, IEffect? oldValue, IEffect? newValue) |
|||
{ |
|||
if ((oldValue != null || newValue != null) |
|||
&& ( |
|||
TryWithAnimator<BlurEffectAnimator, IBlurEffect>(progress, s_blurEffectAnimator, |
|||
oldValue, newValue, s_DefaultBlur, out var observable) |
|||
|| TryWithAnimator<DropShadowEffectAnimator, IDropShadowEffect>(progress, s_dropShadowEffectAnimator, |
|||
oldValue, newValue, s_DefaultDropShadow, out observable) |
|||
)) |
|||
return observable; |
|||
|
|||
return new IncompatibleTransitionObservable(progress, Easing, oldValue, newValue); |
|||
} |
|||
|
|||
private sealed class IncompatibleTransitionObservable : TransitionObservableBase<IEffect?> |
|||
{ |
|||
private readonly IEffect? _from; |
|||
private readonly IEffect? _to; |
|||
|
|||
public IncompatibleTransitionObservable(IObservable<double> progress, Easing easing, IEffect? from, IEffect? to) : base(progress, easing) |
|||
{ |
|||
_from = from; |
|||
_to = to; |
|||
} |
|||
|
|||
protected override IEffect? ProduceValue(double progress) |
|||
{ |
|||
return progress >= 0.5 ? _to : _from; |
|||
} |
|||
} |
|||
} |
|||
@ -0,0 +1,29 @@ |
|||
// ReSharper disable once CheckNamespace
|
|||
|
|||
using Avalonia.Animation.Animators; |
|||
|
|||
namespace Avalonia.Media; |
|||
|
|||
public interface IBlurEffect : IEffect |
|||
{ |
|||
double Radius { get; } |
|||
} |
|||
|
|||
public class ImmutableBlurEffect : IBlurEffect, IImmutableEffect |
|||
{ |
|||
static ImmutableBlurEffect() |
|||
{ |
|||
EffectAnimator.EnsureRegistered(); |
|||
} |
|||
|
|||
public ImmutableBlurEffect(double radius) |
|||
{ |
|||
Radius = radius; |
|||
} |
|||
|
|||
public double Radius { get; } |
|||
|
|||
public bool Equals(IEffect? other) => |
|||
// ReSharper disable once CompareOfFloatsByEqualityOperator
|
|||
other is IBlurEffect blur && blur.Radius == Radius; |
|||
} |
|||
@ -0,0 +1,84 @@ |
|||
// ReSharper disable once CheckNamespace
|
|||
|
|||
using System; |
|||
using Avalonia.Animation.Animators; |
|||
|
|||
namespace Avalonia.Media; |
|||
|
|||
public interface IDropShadowEffect : IEffect |
|||
{ |
|||
double OffsetX { get; } |
|||
double OffsetY { get; } |
|||
double BlurRadius { get; } |
|||
Color Color { get; } |
|||
double Opacity { get; } |
|||
} |
|||
|
|||
internal interface IDirectionDropShadowEffect : IDropShadowEffect |
|||
{ |
|||
double Direction { get; } |
|||
double ShadowDepth { get; } |
|||
} |
|||
|
|||
public class ImmutableDropShadowEffect : IDropShadowEffect, IImmutableEffect |
|||
{ |
|||
static ImmutableDropShadowEffect() |
|||
{ |
|||
EffectAnimator.EnsureRegistered(); |
|||
} |
|||
|
|||
public ImmutableDropShadowEffect(double offsetX, double offsetY, double blurRadius, Color color, double opacity) |
|||
{ |
|||
OffsetX = offsetX; |
|||
OffsetY = offsetY; |
|||
BlurRadius = blurRadius; |
|||
Color = color; |
|||
Opacity = opacity; |
|||
} |
|||
|
|||
public double OffsetX { get; } |
|||
public double OffsetY { get; } |
|||
public double BlurRadius { get; } |
|||
public Color Color { get; } |
|||
public double Opacity { get; } |
|||
public bool Equals(IEffect? other) |
|||
{ |
|||
return other is IDropShadowEffect d |
|||
&& d.OffsetX == OffsetX && d.OffsetY == OffsetY |
|||
&& d.BlurRadius == BlurRadius |
|||
&& d.Color == Color && d.Opacity == Opacity; |
|||
} |
|||
} |
|||
|
|||
|
|||
public class ImmutableDropShadowDirectionEffect : IDirectionDropShadowEffect, IImmutableEffect |
|||
{ |
|||
static ImmutableDropShadowDirectionEffect() |
|||
{ |
|||
EffectAnimator.EnsureRegistered(); |
|||
} |
|||
|
|||
public ImmutableDropShadowDirectionEffect(double direction, double shadowDepth, double blurRadius, Color color, double opacity) |
|||
{ |
|||
Direction = direction; |
|||
ShadowDepth = shadowDepth; |
|||
BlurRadius = blurRadius; |
|||
Color = color; |
|||
Opacity = opacity; |
|||
} |
|||
|
|||
public double OffsetX => Math.Cos(Direction * Math.PI / 180) * ShadowDepth; |
|||
public double OffsetY => Math.Sin(Direction * Math.PI / 180) * ShadowDepth; |
|||
public double Direction { get; } |
|||
public double ShadowDepth { get; } |
|||
public double BlurRadius { get; } |
|||
public Color Color { get; } |
|||
public double Opacity { get; } |
|||
public bool Equals(IEffect? other) |
|||
{ |
|||
return other is IDropShadowEffect d |
|||
&& d.OffsetX == OffsetX && d.OffsetY == OffsetY |
|||
&& d.BlurRadius == BlurRadius |
|||
&& d.Color == Color && d.Opacity == Opacity; |
|||
} |
|||
} |
|||
@ -0,0 +1,26 @@ |
|||
// ReSharper disable once CheckNamespace
|
|||
|
|||
using System; |
|||
using System.ComponentModel; |
|||
|
|||
namespace Avalonia.Media; |
|||
|
|||
[TypeConverter(typeof(EffectConverter))] |
|||
public interface IEffect |
|||
{ |
|||
|
|||
} |
|||
|
|||
public interface IMutableEffect : IEffect, IAffectsRender |
|||
{ |
|||
/// <summary>
|
|||
/// Creates an immutable clone of the effect.
|
|||
/// </summary>
|
|||
/// <returns>The immutable clone.</returns>
|
|||
internal IImmutableEffect ToImmutable(); |
|||
} |
|||
|
|||
public interface IImmutableEffect : IEffect, IEquatable<IEffect> |
|||
{ |
|||
|
|||
} |
|||
@ -0,0 +1,259 @@ |
|||
using System; |
|||
using System.Collections; |
|||
using System.Collections.Concurrent; |
|||
using System.Collections.Generic; |
|||
using System.Diagnostics.CodeAnalysis; |
|||
using System.Globalization; |
|||
using Avalonia.Platform; |
|||
|
|||
namespace Avalonia.Media.Fonts |
|||
{ |
|||
public abstract class FontCollectionBase : IFontCollection |
|||
{ |
|||
protected readonly ConcurrentDictionary<string, ConcurrentDictionary<FontCollectionKey, IGlyphTypeface?>> _glyphTypefaceCache = new(); |
|||
|
|||
public abstract Uri Key { get; } |
|||
|
|||
public abstract int Count { get; } |
|||
|
|||
public abstract FontFamily this[int index] { get; } |
|||
|
|||
public abstract bool TryGetGlyphTypeface(string familyName, FontStyle style, FontWeight weight, FontStretch stretch, |
|||
[NotNullWhen(true)] out IGlyphTypeface? glyphTypeface); |
|||
|
|||
public bool TryMatchCharacter(int codepoint, FontStyle style, FontWeight weight, FontStretch stretch, |
|||
string? familyName, CultureInfo? culture, out Typeface match) |
|||
{ |
|||
match = default; |
|||
|
|||
if (string.IsNullOrEmpty(familyName)) |
|||
{ |
|||
foreach (var typefaces in _glyphTypefaceCache.Values) |
|||
{ |
|||
if (TryGetNearestMatch(typefaces, new FontCollectionKey { Style = style, Weight = weight, Stretch = stretch }, out var glyphTypeface)) |
|||
{ |
|||
if (glyphTypeface.TryGetGlyph((uint)codepoint, out _)) |
|||
{ |
|||
match = new Typeface(glyphTypeface.FamilyName, style, weight, stretch); |
|||
|
|||
return true; |
|||
} |
|||
} |
|||
} |
|||
} |
|||
else |
|||
{ |
|||
if (TryGetGlyphTypeface(familyName, style, weight, stretch, out var glyphTypeface)) |
|||
{ |
|||
if (glyphTypeface.TryGetGlyph((uint)codepoint, out _)) |
|||
{ |
|||
match = new Typeface(familyName, style, weight, stretch); |
|||
|
|||
return true; |
|||
} |
|||
} |
|||
} |
|||
|
|||
return false; |
|||
} |
|||
|
|||
public abstract void Initialize(IFontManagerImpl fontManager); |
|||
|
|||
public abstract IEnumerator<FontFamily> GetEnumerator(); |
|||
|
|||
void IDisposable.Dispose() |
|||
{ |
|||
foreach (var glyphTypefaces in _glyphTypefaceCache.Values) |
|||
{ |
|||
foreach (var pair in glyphTypefaces) |
|||
{ |
|||
pair.Value?.Dispose(); |
|||
} |
|||
} |
|||
|
|||
GC.SuppressFinalize(this); |
|||
} |
|||
|
|||
IEnumerator IEnumerable.GetEnumerator() |
|||
{ |
|||
return GetEnumerator(); |
|||
} |
|||
|
|||
internal static bool TryGetNearestMatch( |
|||
ConcurrentDictionary<FontCollectionKey, |
|||
IGlyphTypeface?> glyphTypefaces, |
|||
FontCollectionKey key, |
|||
[NotNullWhen(true)] out IGlyphTypeface? glyphTypeface) |
|||
{ |
|||
if (glyphTypefaces.TryGetValue(key, out glyphTypeface) && glyphTypeface != null) |
|||
{ |
|||
return true; |
|||
} |
|||
|
|||
if (key.Style != FontStyle.Normal) |
|||
{ |
|||
key = key with { Style = FontStyle.Normal }; |
|||
} |
|||
|
|||
if (key.Stretch != FontStretch.Normal) |
|||
{ |
|||
if (TryFindStretchFallback(glyphTypefaces, key, out glyphTypeface)) |
|||
{ |
|||
return true; |
|||
} |
|||
|
|||
if (key.Weight != FontWeight.Normal) |
|||
{ |
|||
if (TryFindStretchFallback(glyphTypefaces, key with { Weight = FontWeight.Normal }, out glyphTypeface)) |
|||
{ |
|||
return true; |
|||
} |
|||
} |
|||
|
|||
key = key with { Stretch = FontStretch.Normal }; |
|||
} |
|||
|
|||
if (TryFindWeightFallback(glyphTypefaces, key, out glyphTypeface)) |
|||
{ |
|||
return true; |
|||
} |
|||
|
|||
if (TryFindStretchFallback(glyphTypefaces, key, out glyphTypeface)) |
|||
{ |
|||
return true; |
|||
} |
|||
|
|||
//Take the first glyph typeface we can find.
|
|||
foreach (var typeface in glyphTypefaces.Values) |
|||
{ |
|||
if(typeface != null) |
|||
{ |
|||
glyphTypeface = typeface; |
|||
|
|||
return true; |
|||
} |
|||
} |
|||
|
|||
return false; |
|||
} |
|||
|
|||
internal static bool TryFindStretchFallback( |
|||
ConcurrentDictionary<FontCollectionKey, |
|||
IGlyphTypeface?> glyphTypefaces, |
|||
FontCollectionKey key, |
|||
[NotNullWhen(true)] out IGlyphTypeface? glyphTypeface) |
|||
{ |
|||
glyphTypeface = null; |
|||
|
|||
var stretch = (int)key.Stretch; |
|||
|
|||
if (stretch < 5) |
|||
{ |
|||
for (var i = 0; stretch + i < 9; i++) |
|||
{ |
|||
if (glyphTypefaces.TryGetValue(key with { Stretch = (FontStretch)(stretch + i) }, out glyphTypeface) && glyphTypeface != null) |
|||
{ |
|||
return true; |
|||
} |
|||
} |
|||
} |
|||
else |
|||
{ |
|||
for (var i = 0; stretch - i > 1; i++) |
|||
{ |
|||
if (glyphTypefaces.TryGetValue(key with { Stretch = (FontStretch)(stretch - i) }, out glyphTypeface) && glyphTypeface != null) |
|||
{ |
|||
return true; |
|||
} |
|||
} |
|||
} |
|||
|
|||
return false; |
|||
} |
|||
|
|||
internal static bool TryFindWeightFallback( |
|||
ConcurrentDictionary<FontCollectionKey, |
|||
IGlyphTypeface?> glyphTypefaces, |
|||
FontCollectionKey key, |
|||
[NotNullWhen(true)] out IGlyphTypeface? glyphTypeface) |
|||
{ |
|||
glyphTypeface = null; |
|||
var weight = (int)key.Weight; |
|||
|
|||
//If the target weight given is between 400 and 500 inclusive
|
|||
if (weight >= 400 && weight <= 500) |
|||
{ |
|||
//Look for available weights between the target and 500, in ascending order.
|
|||
for (var i = 0; weight + i <= 500; i += 50) |
|||
{ |
|||
if (glyphTypefaces.TryGetValue(key with { Weight = (FontWeight)(weight + i) }, out glyphTypeface) && glyphTypeface != null) |
|||
{ |
|||
return true; |
|||
} |
|||
} |
|||
|
|||
//If no match is found, look for available weights less than the target, in descending order.
|
|||
for (var i = 0; weight - i >= 100; i += 50) |
|||
{ |
|||
if (glyphTypefaces.TryGetValue(key with { Weight = (FontWeight)(weight - i) }, out glyphTypeface) && glyphTypeface != null) |
|||
{ |
|||
return true; |
|||
} |
|||
} |
|||
|
|||
//If no match is found, look for available weights greater than 500, in ascending order.
|
|||
for (var i = 0; weight + i <= 900; i += 50) |
|||
{ |
|||
if (glyphTypefaces.TryGetValue(key with { Weight = (FontWeight)(weight + i) }, out glyphTypeface) && glyphTypeface != null) |
|||
{ |
|||
return true; |
|||
} |
|||
} |
|||
} |
|||
|
|||
//If a weight less than 400 is given, look for available weights less than the target, in descending order.
|
|||
if (weight < 400) |
|||
{ |
|||
for (var i = 0; weight - i >= 100; i += 50) |
|||
{ |
|||
if (glyphTypefaces.TryGetValue(key with { Weight = (FontWeight)(weight - i) }, out glyphTypeface) && glyphTypeface != null) |
|||
{ |
|||
return true; |
|||
} |
|||
} |
|||
|
|||
//If no match is found, look for available weights less than the target, in descending order.
|
|||
for (var i = 0; weight + i <= 900; i += 50) |
|||
{ |
|||
if (glyphTypefaces.TryGetValue(key with { Weight = (FontWeight)(weight + i) }, out glyphTypeface) && glyphTypeface != null) |
|||
{ |
|||
return true; |
|||
} |
|||
} |
|||
} |
|||
|
|||
//If a weight greater than 500 is given, look for available weights greater than the target, in ascending order.
|
|||
if (weight > 500) |
|||
{ |
|||
for (var i = 0; weight + i <= 900; i += 50) |
|||
{ |
|||
if (glyphTypefaces.TryGetValue(key with { Weight = (FontWeight)(weight + i) }, out glyphTypeface) && glyphTypeface != null) |
|||
{ |
|||
return true; |
|||
} |
|||
} |
|||
|
|||
//If no match is found, look for available weights less than the target, in descending order.
|
|||
for (var i = 0; weight - i >= 100; i += 50) |
|||
{ |
|||
if (glyphTypefaces.TryGetValue(key with { Weight = (FontWeight)(weight - i) }, out glyphTypeface) && glyphTypeface != null) |
|||
{ |
|||
return true; |
|||
} |
|||
} |
|||
} |
|||
|
|||
return false; |
|||
} |
|||
} |
|||
} |
|||
Some files were not shown because too many files changed in this diff
Loading…
Reference in new issue