diff --git a/Avalonia.sln b/Avalonia.sln index a0314b1c33..8c857389dc 100644 --- a/Avalonia.sln +++ b/Avalonia.sln @@ -239,7 +239,6 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Solution Items", "Solution NOTICE.md = NOTICE.md NuGet.Config = NuGet.Config readme.md = readme.md - Settings.StyleCop = Settings.StyleCop EndProjectSection EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Avalonia.Generators", "src\tools\Avalonia.Generators\Avalonia.Generators.csproj", "{DDA28789-C21A-4654-86CE-D01E81F095C5}" diff --git a/Directory.Build.props b/Directory.Build.props index 117c0964d2..f124456eab 100644 --- a/Directory.Build.props +++ b/Directory.Build.props @@ -9,5 +9,9 @@ false False 12 + true + true + true + true diff --git a/api/Avalonia.Skia.nupkg.xml b/api/Avalonia.Skia.nupkg.xml index ed29e880a4..b275cbff58 100644 --- a/api/Avalonia.Skia.nupkg.xml +++ b/api/Avalonia.Skia.nupkg.xml @@ -1,6 +1,12 @@  + + CP0002 + M:Avalonia.Skia.SkiaSharpExtensions.ToSKFilterQuality(Avalonia.Media.Imaging.BitmapInterpolationMode) + baseline/netstandard2.0/Avalonia.Skia.dll + target/netstandard2.0/Avalonia.Skia.dll + CP0006 M:Avalonia.Skia.ISkiaGpuWithPlatformGraphicsContext.TryGetGrContext diff --git a/api/Avalonia.nupkg.xml b/api/Avalonia.nupkg.xml index 0be3d69b7b..8d64cb2a82 100644 --- a/api/Avalonia.nupkg.xml +++ b/api/Avalonia.nupkg.xml @@ -7,6 +7,72 @@ baseline/netstandard2.0/Avalonia.Base.dll target/netstandard2.0/Avalonia.Base.dll + + CP0001 + T:Avalonia.Data.Core.CastTypePropertyPathElement + baseline/netstandard2.0/Avalonia.Base.dll + target/netstandard2.0/Avalonia.Base.dll + + + CP0001 + T:Avalonia.Data.Core.ChildTraversalPropertyPathElement + baseline/netstandard2.0/Avalonia.Base.dll + target/netstandard2.0/Avalonia.Base.dll + + + CP0001 + T:Avalonia.Data.Core.EnsureTypePropertyPathElement + baseline/netstandard2.0/Avalonia.Base.dll + target/netstandard2.0/Avalonia.Base.dll + + + CP0001 + T:Avalonia.Data.Core.IPropertyPathElement + baseline/netstandard2.0/Avalonia.Base.dll + target/netstandard2.0/Avalonia.Base.dll + + + CP0001 + T:Avalonia.Data.Core.PropertyPath + baseline/netstandard2.0/Avalonia.Base.dll + target/netstandard2.0/Avalonia.Base.dll + + + CP0001 + T:Avalonia.Data.Core.PropertyPathBuilder + baseline/netstandard2.0/Avalonia.Base.dll + target/netstandard2.0/Avalonia.Base.dll + + + CP0001 + T:Avalonia.Data.Core.PropertyPropertyPathElement + baseline/netstandard2.0/Avalonia.Base.dll + target/netstandard2.0/Avalonia.Base.dll + + + CP0001 + T:Avalonia.Utilities.CharacterReader + baseline/netstandard2.0/Avalonia.Base.dll + target/netstandard2.0/Avalonia.Base.dll + + + CP0001 + T:Avalonia.Utilities.IdentifierParser + baseline/netstandard2.0/Avalonia.Base.dll + target/netstandard2.0/Avalonia.Base.dll + + + CP0001 + T:Avalonia.Utilities.KeywordParser + baseline/netstandard2.0/Avalonia.Base.dll + target/netstandard2.0/Avalonia.Base.dll + + + CP0001 + T:Avalonia.Utilities.StyleClassParser + baseline/netstandard2.0/Avalonia.Base.dll + target/netstandard2.0/Avalonia.Base.dll + CP0002 M:Avalonia.Diagnostics.AppliedStyle.get_HasActivator @@ -43,6 +109,42 @@ baseline/netstandard2.0/Avalonia.Base.dll target/netstandard2.0/Avalonia.Base.dll + + CP0002 + M:Avalonia.Threading.DispatcherPriorityAwaitable.get_IsCompleted + baseline/netstandard2.0/Avalonia.Base.dll + target/netstandard2.0/Avalonia.Base.dll + + + CP0002 + M:Avalonia.Threading.DispatcherPriorityAwaitable.GetAwaiter + baseline/netstandard2.0/Avalonia.Base.dll + target/netstandard2.0/Avalonia.Base.dll + + + CP0002 + M:Avalonia.Threading.DispatcherPriorityAwaitable.GetResult + baseline/netstandard2.0/Avalonia.Base.dll + target/netstandard2.0/Avalonia.Base.dll + + + CP0002 + M:Avalonia.Threading.DispatcherPriorityAwaitable.OnCompleted(System.Action) + baseline/netstandard2.0/Avalonia.Base.dll + target/netstandard2.0/Avalonia.Base.dll + + + CP0002 + M:Avalonia.Threading.DispatcherPriorityAwaitable`1.GetAwaiter + baseline/netstandard2.0/Avalonia.Base.dll + target/netstandard2.0/Avalonia.Base.dll + + + CP0002 + M:Avalonia.Threading.DispatcherPriorityAwaitable`1.GetResult + baseline/netstandard2.0/Avalonia.Base.dll + target/netstandard2.0/Avalonia.Base.dll + CP0002 M:Avalonia.Controls.Primitives.IPopupHost.ConfigurePosition(Avalonia.Visual,Avalonia.Controls.PlacementMode,Avalonia.Point,Avalonia.Controls.Primitives.PopupPositioning.PopupAnchor,Avalonia.Controls.Primitives.PopupPositioning.PopupGravity,Avalonia.Controls.Primitives.PopupPositioning.PopupPositionerConstraintAdjustment,System.Nullable{Avalonia.Rect}) @@ -109,6 +211,42 @@ baseline/netstandard2.0/Avalonia.Controls.dll target/netstandard2.0/Avalonia.Controls.dll + + CP0006 + P:Avalonia.Controls.Platform.IInsetsManager.DisplayEdgeToEdgePreference + baseline/netstandard2.0/Avalonia.Controls.dll + target/netstandard2.0/Avalonia.Controls.dll + + + CP0006 + P:Avalonia.Controls.Platform.IInsetsManager.DisplaysEdgeToEdge + baseline/netstandard2.0/Avalonia.Controls.dll + target/netstandard2.0/Avalonia.Controls.dll + + + CP0007 + T:Avalonia.Threading.DispatcherPriorityAwaitable + baseline/netstandard2.0/Avalonia.Base.dll + target/netstandard2.0/Avalonia.Base.dll + + + CP0007 + T:Avalonia.Threading.DispatcherPriorityAwaitable`1 + baseline/netstandard2.0/Avalonia.Base.dll + target/netstandard2.0/Avalonia.Base.dll + + + CP0008 + T:Avalonia.Threading.DispatcherPriorityAwaitable + baseline/netstandard2.0/Avalonia.Base.dll + target/netstandard2.0/Avalonia.Base.dll + + + CP0008 + T:Avalonia.Threading.DispatcherPriorityAwaitable`1 + baseline/netstandard2.0/Avalonia.Base.dll + target/netstandard2.0/Avalonia.Base.dll + CP0009 T:Avalonia.Diagnostics.StyleDiagnostics diff --git a/build/ExternalConsumers.props b/build/ExternalConsumers.props index 96cf5cc608..79df2f6be4 100644 --- a/build/ExternalConsumers.props +++ b/build/ExternalConsumers.props @@ -30,5 +30,6 @@ + diff --git a/build/SkiaSharp.props b/build/SkiaSharp.props index 5b643efab7..74339fb125 100644 --- a/build/SkiaSharp.props +++ b/build/SkiaSharp.props @@ -1,10 +1,5 @@  - - - - - - + diff --git a/docs/api-compat.md b/docs/api-compat.md new file mode 100644 index 0000000000..1aa4fbd422 --- /dev/null +++ b/docs/api-compat.md @@ -0,0 +1,32 @@ +# API Compatibility + +Avalonia maintains strict **source and binary compatibility** within major versions. Automated API compatibility checks run on every CI build to enforce this policy—builds will fail if breaking changes are detected. + +## When Breaking Changes Are Permitted + +Breaking changes are only acceptable under these specific circumstances and **must be approved** by the Avalonia code team: + +- **Major version targeting**: When `master` branch is targeting a new major version +- **Accidental public APIs**: When code was unintentionally exposed as a public API and is unlikely to have external dependencies +- **Experimental features**: When the API is explicitly marked as unstable or experimental + +## Handling Approved Breaking Changes + +When a breaking change is approved, you can bypass CI validation using an API suppression file: + +1. **Generate suppression file**: Run `nuke --update-api-suppression true` +2. **Commit changes**: Commit the [updated suppression file](../api/) in a separate commit + +> **Note**: The suppression file should only be updated after the breaking change has been reviewed and approved. + +## Baseline Version Configuration + +API changes are validated against a **baseline version**—the reference point for compatibility checks. + +- **Default behavior**: Uses the current major version (e.g., for version 11.0.5, baseline is 11.0.0) +- **Custom baseline**: Override using the [`api-baseline`](https://github.com/AvaloniaUI/Avalonia/blob/56d94d64b9aa6f16200be39b3bcb17f03325b7f9/nukebuild/BuildParameters.cs#L27) parameter with `nuke` +- **Fallback**: When not specified, uses the version defined in [`SharedVersion.props`](https://github.com/AvaloniaUI/Avalonia/blob/56d94d64b9aa6f16200be39b3bcb17f03325b7f9/build/SharedVersion.props#L6) + +## Additional Resources + +- [API Validation Tool Implementation](https://github.com/AvaloniaUI/Avalonia/pull/12072) - Pull request that introduced this feature \ No newline at end of file diff --git a/Documentation/build.md b/docs/build.md similarity index 100% rename from Documentation/build.md rename to docs/build.md diff --git a/docs/debug-xaml-compiler.md b/docs/debug-xaml-compiler.md new file mode 100644 index 0000000000..da260e6d0a --- /dev/null +++ b/docs/debug-xaml-compiler.md @@ -0,0 +1,15 @@ +# Debugging the XAML Compiler + +The Avalonia XAML compiler can be debugged by setting an MSBuild property which triggers a debugger launch during compilation. This allows you to step through the XAML compilation process and troubleshoot issues. + +To enable XAML compiler debugging, set the `AvaloniaXamlIlDebuggerLaunch` MSBuild property to `true` in your project file: + +```xml + + true + +``` + +When this property is enabled, the XAML compiler will call `Debugger.Launch()` on startup, which prompts you to attach a debugger to the compiler process. + +If you're working with the Sandbox project in the Avalonia repository, you can enable debugging by simply uncommenting the property line in the [project file](https://github.com/AvaloniaUI/Avalonia/blob/56d94d64b9aa6f16200be39b3bcb17f03325b7f9/samples/Sandbox/Sandbox.csproj#L8). diff --git a/docs/images/xcode-product-path.png b/docs/images/xcode-product-path.png new file mode 100644 index 0000000000..d254da9875 Binary files /dev/null and b/docs/images/xcode-product-path.png differ diff --git a/docs/index.md b/docs/index.md new file mode 100644 index 0000000000..6adc4fd450 --- /dev/null +++ b/docs/index.md @@ -0,0 +1,19 @@ +# Avalonia Developer Documentation + +This documentation covers Avalonia framework development. For user documentation on how to use Avalonia, visit https://docs.avaloniaui.net/. + +## Getting Started + +- [Building Avalonia](build.md) describes how to build Avalonia from source +- You should read the [Contributing guidelines](../CONTRIBUTING.md) before you start +- We have a [Code of Conduct](../CODE_OF_CONDUCT.md) + +## Development + +- [Debugging the XAML Compiler](debug-xaml-compiler.md) +- [Porting Code from 3rd Party Sources](porting-code-from-3rd-party-sources.md) + +## Releases + +- [API Compatibility](api-compat.md) describes the API compatibility guarantees provided by Avalonia, and exceptions to these guarantees +- Our [Release Process](release.md) describes the process for creating a new Avalonia release diff --git a/docs/macos-native.md b/docs/macos-native.md new file mode 100644 index 0000000000..5c16742963 --- /dev/null +++ b/docs/macos-native.md @@ -0,0 +1,71 @@ +# macOS Native Code + +The macOS platform backend has a native component, written in objective-c and located in [`native/Avalonia.Native/src/OSX`](../native/Avalonia.Native/src/OSX/). To work on this code, open the [`Avalonia.Native.OSX.xcodeproj`](../native/Avalonia.Native/src/OSX/Avalonia.Native.OSX.xcodeproj)project in Xcode. + +Changes to the native portion of the code can be recompiled in two ways: + +1. Using the [`build.sh`](build.md#build-native-libraries-macos-only) script: this has the downside that it will trigger a full recompile of Avalonia +2. Using `AvaloniaNativeLibraryPath` described below + +## Using `AvaloniaNativeLibraryPath ` + +When you make changes in Xcode and recompile using Cmd+B, the binary will be compiled to a location that can be seen under "Products": + +![How to find the product path in Xcode](images/xcode-product-path.png) + +To use this build in Avalonia, one can specifiy the path by using `AvaloniaNativePlatformOptions.AvaloniaNativeLibraryPath` in an application's AppBuilder: + +```csharp +public static AppBuilder BuildAvaloniaApp() => + AppBuilder.Configure() + .UsePlatformDetect() + .With(new AvaloniaNativePlatformOptions + { + AvaloniaNativeLibraryPath = "[Path to your dylib]", + }) +``` + +# Bundling Development Code + +In certain situations you need to run an Avalonia sample application as an app bundle. One of these situations is testing macOS Accessibility - Xcode's Accessibility Inspector fails to recognise the application otherwise. To facilitate this, the [`IntegrationTestApp`](../samples/IntegrationTestApp/) has a [`bundle.sh`](../samples/IntegrationTestApp/bundle.sh) script which can be run to create a bundle of that application. + +Alteratively, if you need to bundle another project, another solution is to change the sample's output path to resemble an app bundle. You can do this by modifying the output path in the csproj, e.g.: + +```xml +bin\$(Configuration)\$(Platform)\ControlCatalog.NetCore.app/Contents/MacOS +false +true +``` + +And in the Contents output directory place a valid `Info.plist` file. An example for ControlCatalog.NetCore is: + +```xml + + + + + CFBundleName + ControlCatalog.NetCore + CFBundleDisplayName + ControlCatalog.NetCore + CFBundleIdentifier + ControlCatalog.NetCore + CFBundleVersion + 0.10.999 + CFBundlePackageType + AAPL + CFBundleSignature + ???? + CFBundleExecutable + ControlCatalog.NetCore + CFBundleIconFile + ControlCatalog.NetCore.icns + CFBundleShortVersionString + 0.1 + NSPrincipalClass + NSApplication + NSHighResolutionCapable + + + +``` diff --git a/docs/porting-code-from-3rd-party-sources.md b/docs/porting-code-from-3rd-party-sources.md new file mode 100644 index 0000000000..9aa72b8fce --- /dev/null +++ b/docs/porting-code-from-3rd-party-sources.md @@ -0,0 +1,12 @@ +# Porting Code from 3rd Party Sources + +When porting code or adapting code from other projects with a MIT compatible license, the source file must contain appropriate license headers. For example when porting code from WPF the header should contain: + +``` +// This source file is adapted from the Windows Presentation Foundation project. +// (https://github.com/dotnet/wpf/) +// +// Licensed to The Avalonia Project under MIT License, courtesy of The .NET Foundation. +``` + +If the file is a port of a specific commit of file from a 3rd party source, then consider including a permalink to the source file. \ No newline at end of file diff --git a/docs/release.md b/docs/release.md new file mode 100644 index 0000000000..00c83cc47c --- /dev/null +++ b/docs/release.md @@ -0,0 +1,30 @@ +# Release Process + +This document describes the process for creating a new Avalonia release + +## Backports + +- Go through PRs with "backport-candidate-*" tags, make sure this list makes sense +- Checkout https://github.com/grokys/avalonia-backport tool from the source code, get github api token +- Checkout stable release branch, like `release/11.0` +- Run backport cherry-picking process to the stable release branch +- Test if everything builds and tests are passing +- Test nightly builds of Avalonia +- Run labeling process (automatically replace "backport-candidate" with "backported" tags) and generate changelog + +## Release + +- Create a branch named e.g. `release/11.0.9` for the specific minor version +- Update the version number in the file [SharedVersion.props](../build/SharedVersion.props), e.g. `11.0.9` +- Add a tag for this version, e.g. `git tag 11.0.9` +- Push the release branch and the tag. +- Wait for azure pipelines to finish the build. Nightly build with 11.0.9 version should be released soon after. +- Using the nightly build run a due diligence test to make sure you're happy with the package. +- On azure pipelines, on the release for your release branch `release/11.0.9` click on the badge for "Nuget Release" +- Click deploy +- Make a release on Github releases +- Press "Auto-generate changelog", so GitHub will append information about new contributors +- Replace changelog with one generated by avalonia-backport tool. Enable discussion for the specific release +- Review the release information and publish. +- Update the dotnet templates, visual studio templates. +- Announce on telegram (RU and EN), twitter, etc \ No newline at end of file diff --git a/global.json b/global.json index 28c43eff46..f496cffc50 100644 --- a/global.json +++ b/global.json @@ -1,6 +1,6 @@ { "sdk": { - "version": "8.0.404", + "version": "8.0.411", "rollForward": "latestFeature" }, "msbuild-sdks": { diff --git a/native/Avalonia.Native/src/OSX/AvnView.mm b/native/Avalonia.Native/src/OSX/AvnView.mm index 2799425638..4cc495f321 100644 --- a/native/Avalonia.Native/src/OSX/AvnView.mm +++ b/native/Avalonia.Native/src/OSX/AvnView.mm @@ -13,7 +13,6 @@ { ComObjectWeakPtr _parent; NSTrackingArea* _area; - bool _isLeftPressed, _isMiddlePressed, _isRightPressed, _isXButton1Pressed, _isXButton2Pressed; AvnInputModifiers _modifierState; NSEvent* _lastMouseDownEvent; AvnPixelSize _lastPixelSize; @@ -386,11 +385,57 @@ static void ConvertTilt(NSPoint tilt, float* xTilt, float* yTilt) } - (BOOL) resignFirstResponder +{ + auto window = [self window]; + if (window != nullptr && window.keyWindow) + { + [self onLostFocus]; + } + + return YES; +} + +- (void)viewWillMoveToWindow:(NSWindow *)newWindow +{ + auto oldWindow = [self window]; + if (oldWindow == newWindow) + { + // viewWillMoveToWindow can be called with the same window when the view hierarchy changes + return; + } + + if (oldWindow != nullptr) + { + [[NSNotificationCenter defaultCenter] + removeObserver:self + name:@"NSWindowDidResignKeyNotification" + object: oldWindow]; + } + + if (newWindow != nullptr) + { + [[NSNotificationCenter defaultCenter] + addObserver:self + selector:@selector(windowDidResignKey:) + name:@"NSWindowDidResignKeyNotification" + object: newWindow]; + } +} + +- (void)windowDidResignKey:(NSNotification*)notification +{ + auto window = [self window]; + if (window != nullptr && notification.object == window && [window firstResponder] == self) + { + [self onLostFocus]; + } +} + +- (void)onLostFocus { auto parent = _parent.tryGet(); - if(parent) + if (parent) parent->TopLevelEvents->LostFocus(); - return YES; } - (void)mouseMoved:(NSEvent *)event @@ -400,7 +445,6 @@ static void ConvertTilt(NSPoint tilt, float* xTilt, float* yTilt) - (void)mouseDown:(NSEvent *)event { - _isLeftPressed = true; _lastMouseDownEvent = event; [self mouseEvent:event withType:LeftButtonDown]; } @@ -413,15 +457,12 @@ static void ConvertTilt(NSPoint tilt, float* xTilt, float* yTilt) { case 2: case 3: - _isMiddlePressed = true; [self mouseEvent:event withType:MiddleButtonDown]; break; case 4: - _isXButton1Pressed = true; [self mouseEvent:event withType:XButton1Down]; break; case 5: - _isXButton2Pressed = true; [self mouseEvent:event withType:XButton2Down]; break; @@ -432,14 +473,12 @@ static void ConvertTilt(NSPoint tilt, float* xTilt, float* yTilt) - (void)rightMouseDown:(NSEvent *)event { - _isRightPressed = true; _lastMouseDownEvent = event; [self mouseEvent:event withType:RightButtonDown]; } - (void)mouseUp:(NSEvent *)event { - _isLeftPressed = false; [self mouseEvent:event withType:LeftButtonUp]; } @@ -449,15 +488,12 @@ static void ConvertTilt(NSPoint tilt, float* xTilt, float* yTilt) { case 2: case 3: - _isMiddlePressed = false; [self mouseEvent:event withType:MiddleButtonUp]; break; case 4: - _isXButton1Pressed = false; [self mouseEvent:event withType:XButton1Up]; break; case 5: - _isXButton2Pressed = false; [self mouseEvent:event withType:XButton2Up]; break; @@ -468,7 +504,6 @@ static void ConvertTilt(NSPoint tilt, float* xTilt, float* yTilt) - (void)rightMouseUp:(NSEvent *)event { - _isRightPressed = false; [self mouseEvent:event withType:RightButtonUp]; } @@ -551,15 +586,6 @@ static void ConvertTilt(NSPoint tilt, float* xTilt, float* yTilt) _modifierState = [self getModifiers:modifierFlags]; } -- (void)resetPressedMouseButtons -{ - _isLeftPressed = false; - _isRightPressed = false; - _isMiddlePressed = false; - _isXButton1Pressed = false; - _isXButton2Pressed = false; -} - - (void)flagsChanged:(NSEvent *)event { auto newModifierState = [self getModifiers:[event modifierFlags]]; @@ -706,15 +732,17 @@ static void ConvertTilt(NSPoint tilt, float* xTilt, float* yTilt) if (mod & NSEventModifierFlagCommand) rv |= Windows; - if (_isLeftPressed) + NSUInteger pressedButtons = [NSEvent pressedMouseButtons]; + + if (pressedButtons & (1 << 0)) // Left mouse button rv |= LeftMouseButton; - if (_isMiddlePressed) - rv |= MiddleMouseButton; - if (_isRightPressed) + if (pressedButtons & (1 << 1)) // Right mouse button rv |= RightMouseButton; - if (_isXButton1Pressed) + if (pressedButtons & (1 << 2)) // Middle mouse button + rv |= MiddleMouseButton; + if (pressedButtons & (1 << 3)) // X1 button rv |= XButton1MouseButton; - if (_isXButton2Pressed) + if (pressedButtons & (1 << 4)) // X2 button rv |= XButton2MouseButton; return (AvnInputModifiers)rv; diff --git a/native/Avalonia.Native/src/OSX/AvnWindow.mm b/native/Avalonia.Native/src/OSX/AvnWindow.mm index 2dee90bfa3..03daa2f296 100644 --- a/native/Avalonia.Native/src/OSX/AvnWindow.mm +++ b/native/Avalonia.Native/src/OSX/AvnWindow.mm @@ -435,9 +435,23 @@ return; } - if(window->WindowState() == Maximized) + // If the window has been moved into a position where it's "zoomed" + // Then it should be set as Maximized. + if (window->WindowState() != Maximized && window->IsZoomed()) { - window->SetWindowState(Normal); + window->SetWindowState(Maximized, false); + } + // We should only return the window state to normal if + // the internal window state is maximized, and macOS says + // the window is no longer zoomed (I.E, the user has moved it) + // Stage Manager will "move" the window when repositioning it + // So if the window was "maximized" before, it should stay maximized + else if(window->WindowState() == Maximized && !window->IsZoomed()) + { + // If we're moving the window while maximized, + // we need to let macOS handle if it should be resized + // And not handle it ourselves. + window->SetWindowState(Normal, false); } } @@ -462,8 +476,95 @@ } } +- (BOOL)isPointInTitlebar:(NSPoint)windowPoint +{ + auto parent = _parent.tryGetWithCast(); + if (!parent || !_isExtended) { + return NO; + } + + AvnView* view = parent->View; + NSPoint viewPoint = [view convertPoint:windowPoint fromView:nil]; + double titlebarHeight = [self getExtendedTitleBarHeight]; + + // Check if click is in titlebar area (top portion of view) + if (viewPoint.y <= titlebarHeight) { + // Verify we're actually in a toolbar-related area + NSView* hitView = [[self findRootView:view] hitTest:windowPoint]; + if (hitView) { + NSString* hitViewClass = [hitView className]; + if ([hitViewClass containsString:@"Toolbar"] || [hitViewClass containsString:@"Titlebar"]) { + return YES; + } + } + } + return NO; +} + +- (void)forwardToAvnView:(NSEvent *)event +{ + auto parent = _parent.tryGetWithCast(); + if (!parent) { + return; + } + + switch(event.type) { + case NSEventTypeLeftMouseDown: + [parent->View mouseDown:event]; + break; + case NSEventTypeLeftMouseUp: + [parent->View mouseUp:event]; + break; + case NSEventTypeLeftMouseDragged: + [parent->View mouseDragged:event]; + break; + case NSEventTypeRightMouseDown: + [parent->View rightMouseDown:event]; + break; + case NSEventTypeRightMouseUp: + [parent->View rightMouseUp:event]; + break; + case NSEventTypeRightMouseDragged: + [parent->View rightMouseDragged:event]; + break; + case NSEventTypeOtherMouseDown: + [parent->View otherMouseDown:event]; + break; + case NSEventTypeOtherMouseUp: + [parent->View otherMouseUp:event]; + break; + case NSEventTypeOtherMouseDragged: + [parent->View otherMouseDragged:event]; + break; + case NSEventTypeMouseMoved: + [parent->View mouseMoved:event]; + break; + default: + break; + } +} + - (void)sendEvent:(NSEvent *_Nonnull)event { + // Event-tracking loop for thick titlebar mouse events + if (event.type == NSEventTypeLeftMouseDown && [self isPointInTitlebar:event.locationInWindow]) + { + NSEventMask mask = NSEventMaskLeftMouseDragged | NSEventMaskLeftMouseUp; + NSEvent *ev = event; + while (ev.type != NSEventTypeLeftMouseUp) + { + [self forwardToAvnView:ev]; + [super sendEvent:ev]; + ev = [NSApp nextEventMatchingMask:mask + untilDate:[NSDate distantFuture] + inMode:NSEventTrackingRunLoopMode + dequeue:YES]; + } + [self forwardToAvnView:ev]; + [super sendEvent:ev]; + return; + } + [super sendEvent:event]; auto parent = _parent.tryGetWithCast(); diff --git a/native/Avalonia.Native/src/OSX/StorageProvider.mm b/native/Avalonia.Native/src/OSX/StorageProvider.mm index abf7f85c5f..92278a85e9 100644 --- a/native/Avalonia.Native/src/OSX/StorageProvider.mm +++ b/native/Avalonia.Native/src/OSX/StorageProvider.mm @@ -235,11 +235,6 @@ public: panel.title = [NSString stringWithUTF8String:title]; } - if(initialDirectory != nullptr) - { - auto directoryString = [NSString stringWithUTF8String:initialDirectory]; - panel.directoryURL = [NSURL URLWithString:directoryString]; - } if(initialFile != nullptr) { @@ -248,6 +243,12 @@ public: SetAccessoryView(panel, filters, false); + if(initialDirectory != nullptr) + { + auto directoryString = [NSString stringWithUTF8String:initialDirectory]; + panel.directoryURL = [NSURL URLWithString:directoryString]; + } + auto handler = ^(NSModalResponse result) { if(result == NSFileHandlingPanelOKButton) { @@ -304,11 +305,6 @@ public: panel.title = [NSString stringWithUTF8String:title]; } - if(initialDirectory != nullptr) - { - auto directoryString = [NSString stringWithUTF8String:initialDirectory]; - panel.directoryURL = [NSURL URLWithString:directoryString]; - } if(initialFile != nullptr) { @@ -317,6 +313,12 @@ public: SetAccessoryView(panel, filters, true); + if(initialDirectory != nullptr) + { + auto directoryString = [NSString stringWithUTF8String:initialDirectory]; + panel.directoryURL = [NSURL URLWithString:directoryString]; + } + auto handler = ^(NSModalResponse result) { if(result == NSFileHandlingPanelOKButton) { diff --git a/native/Avalonia.Native/src/OSX/WindowImpl.h b/native/Avalonia.Native/src/OSX/WindowImpl.h index fce7273f30..37699082ed 100644 --- a/native/Avalonia.Native/src/OSX/WindowImpl.h +++ b/native/Avalonia.Native/src/OSX/WindowImpl.h @@ -71,6 +71,8 @@ BEGIN_INTERFACE_MAP() void ExitFullScreenMode (); virtual HRESULT SetWindowState (AvnWindowState state) override; + + virtual HRESULT SetWindowState (AvnWindowState state, bool shouldResize); virtual bool IsModal() override; diff --git a/native/Avalonia.Native/src/OSX/WindowImpl.mm b/native/Avalonia.Native/src/OSX/WindowImpl.mm index 03f3319bcd..341085ec08 100644 --- a/native/Avalonia.Native/src/OSX/WindowImpl.mm +++ b/native/Avalonia.Native/src/OSX/WindowImpl.mm @@ -451,6 +451,10 @@ void WindowImpl::ExitFullScreenMode() { } HRESULT WindowImpl::SetWindowState(AvnWindowState state) { + return SetWindowState(state, true); +} + +HRESULT WindowImpl::SetWindowState(AvnWindowState state, bool shouldResize) { START_COM_CALL; @autoreleasepool { @@ -474,61 +478,63 @@ HRESULT WindowImpl::SetWindowState(AvnWindowState state) { if (_shown) { _actualWindowState = _lastWindowState; - switch (state) { - case Maximized: - if (currentState == FullScreen) { - ExitFullScreenMode(); - } + if (shouldResize) { + switch (state) { + case Maximized: + if (currentState == FullScreen) { + ExitFullScreenMode(); + } - lastPositionSet.X = 0; - lastPositionSet.Y = 0; + lastPositionSet.X = 0; + lastPositionSet.Y = 0; - if ([Window isMiniaturized]) { - [Window deminiaturize:Window]; - } + if ([Window isMiniaturized]) { + [Window deminiaturize:Window]; + } - if (!IsZoomed()) { - DoZoom(); - } - break; + if (!IsZoomed()) { + DoZoom(); + } + break; - case Minimized: - if (currentState == FullScreen) { - ExitFullScreenMode(); - } else { - [Window miniaturize:Window]; - } - break; + case Minimized: + if (currentState == FullScreen) { + ExitFullScreenMode(); + } else { + [Window miniaturize:Window]; + } + break; - case FullScreen: - if ([Window isMiniaturized]) { - [Window deminiaturize:Window]; - } + case FullScreen: + if ([Window isMiniaturized]) { + [Window deminiaturize:Window]; + } - EnterFullScreenMode(); - break; + EnterFullScreenMode(); + break; - case Normal: - if ([Window isMiniaturized]) { - [Window deminiaturize:Window]; - } + case Normal: + if ([Window isMiniaturized]) { + [Window deminiaturize:Window]; + } - if (currentState == FullScreen) { - ExitFullScreenMode(); - } + if (currentState == FullScreen) { + ExitFullScreenMode(); + } - if (IsZoomed()) { - if (_decorations == SystemDecorationsFull) { - DoZoom(); - } else { - [Window setFrame:_preZoomSize display:true]; - auto newFrame = [Window contentRectForFrameRect:[Window frame]].size; + if (IsZoomed()) { + if (_decorations == SystemDecorationsFull) { + DoZoom(); + } else { + [Window setFrame:_preZoomSize display:true]; + auto newFrame = [Window contentRectForFrameRect:[Window frame]].size; - [View setFrameSize:newFrame]; - } + [View setFrameSize:newFrame]; + } - } - break; + } + break; + } } WindowEvents->WindowStateChanged(_actualWindowState); diff --git a/native/Avalonia.Native/src/OSX/app.mm b/native/Avalonia.Native/src/OSX/app.mm index dfa4062f0a..5dc994fb6b 100644 --- a/native/Avalonia.Native/src/OSX/app.mm +++ b/native/Avalonia.Native/src/OSX/app.mm @@ -30,8 +30,6 @@ ComPtr _events; break; } - [[NSApplication sharedApplication] setActivationPolicy: AvnDesiredActivationPolicy]; - [[NSUserDefaults standardUserDefaults] setBool:NO forKey:@"NSFullScreenMenuItemEverywhere"]; [[NSApplication sharedApplication] setHelpMenu: [[NSMenu new] initWithTitle:@""]]; diff --git a/native/Avalonia.Native/src/OSX/main.mm b/native/Avalonia.Native/src/OSX/main.mm index d1dbe9d186..0e3621517e 100644 --- a/native/Avalonia.Native/src/OSX/main.mm +++ b/native/Avalonia.Native/src/OSX/main.mm @@ -129,14 +129,22 @@ public: } } - virtual HRESULT SetShowInDock(int show) override + virtual HRESULT SetShowInDock(int show) override { START_COM_CALL; @autoreleasepool { - AvnDesiredActivationPolicy = show - ? NSApplicationActivationPolicyRegular : NSApplicationActivationPolicyAccessory; + NSApplication* app = [NSApplication sharedApplication]; + NSApplicationActivationPolicy requestedPolicy = show + ? NSApplicationActivationPolicyRegular + : NSApplicationActivationPolicyAccessory; + + if ([app activationPolicy] != requestedPolicy) + { + [app setActivationPolicy:requestedPolicy]; + } + return S_OK; } } diff --git a/readme.md b/readme.md index 5954a81ac5..08fd014798 100644 --- a/readme.md +++ b/readme.md @@ -28,7 +28,7 @@ You can also see what [breaking changes](https://github.com/AvaloniaUI/Avalonia/ See our [Get Started](https://avaloniaui.net/gettingstarted?utm_source=github&utm_medium=referral&utm_content=readme_link) guide to begin developing apps with Avalonia UI. ### Visual Studio -The Avalonia [Visual Studio Extension](https://marketplace.visualstudio.com/items?itemName=AvaloniaTeam.AvaloniaforVisualStudio) contains project and control templates that will help you get started, or you can use the .NET Core CLI. For a starter guide see our [documentation](https://docs.avaloniaui.net/docs/getting-started). +The Avalonia [Visual Studio Extension](https://marketplace.visualstudio.com/items?itemName=AvaloniaTeam.AvaloniaVS) contains project and control templates that will help you get started, or you can use the .NET Core CLI. For a starter guide see our [documentation](https://docs.avaloniaui.net/docs/getting-started). ### JetBrains Rider [JetBrains Rider](https://www.jetbrains.com/rider/whatsnew/?mkt_tok=eyJpIjoiTURBNU1HSmhNV0kwTUdFMiIsInQiOiJtNnU2VEc1TlNLa1ZRVkROYmdZYVpYREJsaU1qdUhmS3dxSzRHczdYWHl0RVlTNDMwSFwvNUs3VENTNVM0bVcyNFdaRmVYZzVWTTF1N3VrQWNGTkJreEhlam1hMlB4UVVWcHBGM1dNOUxoXC95YnRQdGgyUXl1YmZCM3h3d3BVWWdBIn0%3D#avalonia-support) now has official support for Avalonia. @@ -49,6 +49,12 @@ Install-Package Avalonia.Desktop See what others have built with Avalonia UI on our [Showcase](https://avaloniaui.net/showcase?utm_source=github&utm_medium=referral&utm_content=readme_link). We welcome submissions! +## Sponsors +Avalonia development is supported by the generous sponsorship of [Devolutions](https://devolutions.net/?utm_source=pr&utm_medium=partnership&utm_campaign=avalonia&utm_id=C086&member_status=responded). + +devolutions-color-hr + + ## Community Join our community hub to get early access to upcoming features, share your thoughts, and connect directly with the Avalonia team. [![communityannouncement-banner 1](https://github.com/user-attachments/assets/21950b56-cd28-4574-9a0a-73bb17b89d31)](https://avaloniaui.community) @@ -70,7 +76,7 @@ We have a [range of samples](https://github.com/AvaloniaUI/Avalonia.Samples) to ## Building and Using -See the [build instructions here](Documentation/build.md). +See the [build instructions here](docs/build.md). ## Contributing diff --git a/samples/ControlCatalog.iOS/Info.plist b/samples/ControlCatalog.iOS/Info.plist index b4c7c07eb6..a1aa23e506 100644 --- a/samples/ControlCatalog.iOS/Info.plist +++ b/samples/ControlCatalog.iOS/Info.plist @@ -16,7 +16,6 @@ 1 2 - 3 UIRequiredDeviceCapabilities @@ -38,5 +37,7 @@ UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight + com.apple.security.files.user-selected.read-write + diff --git a/samples/ControlCatalog/App.xaml.cs b/samples/ControlCatalog/App.xaml.cs index a29a23ff61..b08df1223d 100644 --- a/samples/ControlCatalog/App.xaml.cs +++ b/samples/ControlCatalog/App.xaml.cs @@ -44,6 +44,10 @@ namespace ControlCatalog { desktopLifetime.MainWindow = new MainWindow { DataContext = new MainWindowViewModel() }; } + else if(ApplicationLifetime is IActivityApplicationLifetime singleViewFactoryApplicationLifetime) + { + singleViewFactoryApplicationLifetime.MainViewFactory = () => new MainView { DataContext = new MainWindowViewModel() }; + } else if (ApplicationLifetime is ISingleViewApplicationLifetime singleViewLifetime) { singleViewLifetime.MainView = new MainView { DataContext = new MainWindowViewModel() }; @@ -97,6 +101,10 @@ namespace ControlCatalog newWindow.Show(); oldWindow?.Close(); } + else if (app.ApplicationLifetime is IActivityApplicationLifetime singleViewFactoryApplicationLifetime) + { + singleViewFactoryApplicationLifetime.MainViewFactory = () => new MainView { DataContext = new MainWindowViewModel() }; + } else if (app.ApplicationLifetime is ISingleViewApplicationLifetime singleViewLifetime) { singleViewLifetime.MainView = new MainView(); diff --git a/samples/ControlCatalog/MainView.xaml.cs b/samples/ControlCatalog/MainView.xaml.cs index 89bccb4475..1fefe766e2 100644 --- a/samples/ControlCatalog/MainView.xaml.cs +++ b/samples/ControlCatalog/MainView.xaml.cs @@ -108,14 +108,14 @@ namespace ControlCatalog ViewModel.SafeAreaPadding = insets.SafeAreaPadding; }; - ViewModel.DisplayEdgeToEdge = insets.DisplayEdgeToEdge; + ViewModel.DisplayEdgeToEdge = insets.DisplayEdgeToEdgePreference; ViewModel.IsSystemBarVisible = insets.IsSystemBarVisible ?? true; ViewModel.PropertyChanged += async (sender, args) => { if (args.PropertyName == nameof(ViewModel.DisplayEdgeToEdge)) { - insets.DisplayEdgeToEdge = ViewModel.DisplayEdgeToEdge; + insets.DisplayEdgeToEdgePreference = ViewModel.DisplayEdgeToEdge; } else if (args.PropertyName == nameof(ViewModel.IsSystemBarVisible)) { @@ -124,7 +124,7 @@ namespace ControlCatalog // Give the OS some time to apply new values and refresh the view model. await Task.Delay(100); - ViewModel.DisplayEdgeToEdge = insets.DisplayEdgeToEdge; + ViewModel.DisplayEdgeToEdge = insets.DisplayEdgeToEdgePreference; ViewModel.IsSystemBarVisible = insets.IsSystemBarVisible ?? true; }; } diff --git a/samples/ControlCatalog/Pages/HeaderedContentPage.axaml b/samples/ControlCatalog/Pages/HeaderedContentPage.axaml index ff44c2206d..86a2b00b72 100644 --- a/samples/ControlCatalog/Pages/HeaderedContentPage.axaml +++ b/samples/ControlCatalog/Pages/HeaderedContentPage.axaml @@ -13,6 +13,9 @@ CornerRadius="3"> + + + diff --git a/samples/ControlCatalog/Pages/ListBoxPage.xaml b/samples/ControlCatalog/Pages/ListBoxPage.xaml index 7694845009..e3a706bfed 100644 --- a/samples/ControlCatalog/Pages/ListBoxPage.xaml +++ b/samples/ControlCatalog/Pages/ListBoxPage.xaml @@ -20,6 +20,9 @@ + Hosts a collection of ListBoxItem. diff --git a/samples/ControlCatalog/Pages/SliderPage.xaml b/samples/ControlCatalog/Pages/SliderPage.xaml index 9aa4322ad5..7821e885ba 100644 --- a/samples/ControlCatalog/Pages/SliderPage.xaml +++ b/samples/ControlCatalog/Pages/SliderPage.xaml @@ -21,6 +21,15 @@ IsSnapToTickEnabled="True" Ticks="0,20,25,40,75,100" Width="300" /> + - diff --git a/src/Avalonia.Themes.Fluent/Controls/FluentControls.xaml b/src/Avalonia.Themes.Fluent/Controls/FluentControls.xaml index 90279214e5..18996d6a2d 100644 --- a/src/Avalonia.Themes.Fluent/Controls/FluentControls.xaml +++ b/src/Avalonia.Themes.Fluent/Controls/FluentControls.xaml @@ -22,6 +22,7 @@ + diff --git a/src/Avalonia.Themes.Fluent/Controls/GroupBox.xaml b/src/Avalonia.Themes.Fluent/Controls/GroupBox.xaml new file mode 100644 index 0000000000..1fee84269d --- /dev/null +++ b/src/Avalonia.Themes.Fluent/Controls/GroupBox.xaml @@ -0,0 +1,83 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/Avalonia.Themes.Fluent/Controls/Slider.xaml b/src/Avalonia.Themes.Fluent/Controls/Slider.xaml index 5834df311c..c7e41e1a02 100644 --- a/src/Avalonia.Themes.Fluent/Controls/Slider.xaml +++ b/src/Avalonia.Themes.Fluent/Controls/Slider.xaml @@ -129,6 +129,7 @@ VerticalAlignment="Bottom" Placement="Top" IsVisible="False" + IsDirectionReversed="{TemplateBinding IsDirectionReversed}" Fill="{DynamicResource SliderTickBarFill}"/> - m10.051 7.0032c2.215 0 4.0105 1.7901 4.0105 3.9984s-1.7956 3.9984-4.0105 3.9984c-2.215 0-4.0105-1.7901-4.0105-3.9984s1.7956-3.9984 4.0105-3.9984zm0 1.4994c-1.3844 0-2.5066 1.1188-2.5066 2.499s1.1222 2.499 2.5066 2.499 2.5066-1.1188 2.5066-2.499-1.1222-2.499-2.5066-2.499zm0-5.0026c4.6257 0 8.6188 3.1487 9.7267 7.5613 0.10085 0.40165-0.14399 0.80877-0.54686 0.90931-0.40288 0.10054-0.81122-0.14355-0.91208-0.54521-0.94136-3.7492-4.3361-6.4261-8.2678-6.4261-3.9334 0-7.3292 2.6792-8.2689 6.4306-0.10063 0.40171-0.50884 0.64603-0.91177 0.54571s-0.648-0.5073-0.54737-0.90901c1.106-4.4152 5.1003-7.5667 9.728-7.5667z m0.21967 0.21965c-0.26627 0.26627-0.29047 0.68293-0.07262 0.97654l0.07262 0.08412 4.0346 4.0346c-1.922 1.3495-3.3585 3.365-3.9554 5.7495-0.10058 0.4018 0.14362 0.8091 0.54543 0.9097 0.40182 0.1005 0.80909-0.1436 0.90968-0.5455 0.52947-2.1151 1.8371-3.8891 3.5802-5.0341l1.8096 1.8098c-0.70751 0.7215-1.1438 1.71-1.1438 2.8003 0 2.2092 1.7909 4 4 4 1.0904 0 2.0788-0.4363 2.8004-1.1438l5.9193 5.9195c0.2929 0.2929 0.7677 0.2929 1.0606 0 0.2663-0.2662 0.2905-0.6829 0.0726-0.9765l-0.0726-0.0841-6.1135-6.1142 0.0012-0.0015-1.2001-1.1979-2.8699-2.8693 2e-3 -8e-4 -2.8812-2.8782 0.0012-0.0018-1.1333-1.1305-4.3064-4.3058c-0.29289-0.29289-0.76777-0.29289-1.0607 0zm7.9844 9.0458 3.5351 3.5351c-0.45 0.4358-1.0633 0.704-1.7392 0.704-1.3807 0-2.5-1.1193-2.5-2.5 0-0.6759 0.26824-1.2892 0.7041-1.7391zm1.7959-5.7655c-1.0003 0-1.9709 0.14807-2.8889 0.425l1.237 1.2362c0.5358-0.10587 1.0883-0.16119 1.6519-0.16119 3.9231 0 7.3099 2.6803 8.2471 6.4332 0.1004 0.4018 0.5075 0.6462 0.9094 0.5459 0.4019-0.1004 0.6463-0.5075 0.5459-0.9094-1.103-4.417-5.0869-7.5697-9.7024-7.5697zm0.1947 3.5093 3.8013 3.8007c-0.1018-2.0569-1.7488-3.7024-3.8013-3.8007z - + - + diff --git a/src/Avalonia.Themes.Fluent/Controls/TreeViewItem.xaml b/src/Avalonia.Themes.Fluent/Controls/TreeViewItem.xaml index c92774bc10..a1b22528c9 100644 --- a/src/Avalonia.Themes.Fluent/Controls/TreeViewItem.xaml +++ b/src/Avalonia.Themes.Fluent/Controls/TreeViewItem.xaml @@ -25,9 +25,6 @@ 12, 0, 12, 0 M 1,0 10,10 l -9,10 -1,-1 L 8,10 -0,1 Z M0,1 L10,10 20,1 19,0 10,8 1,0 Z - @@ -76,8 +73,13 @@ MinHeight="{TemplateBinding MinHeight}" TemplatedControl.IsTemplateFocusTarget="True"> + ColumnDefinitions="Auto, *"> + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/Avalonia.Themes.Simple/Controls/SimpleControls.xaml b/src/Avalonia.Themes.Simple/Controls/SimpleControls.xaml index a13f579567..51171051d1 100644 --- a/src/Avalonia.Themes.Simple/Controls/SimpleControls.xaml +++ b/src/Avalonia.Themes.Simple/Controls/SimpleControls.xaml @@ -29,6 +29,7 @@ + diff --git a/src/Avalonia.Themes.Simple/Controls/TextBox.xaml b/src/Avalonia.Themes.Simple/Controls/TextBox.xaml index b5cce6e93e..867d5367c3 100644 --- a/src/Avalonia.Themes.Simple/Controls/TextBox.xaml +++ b/src/Avalonia.Themes.Simple/Controls/TextBox.xaml @@ -5,7 +5,7 @@ m10.051 7.0032c2.215 0 4.0105 1.7901 4.0105 3.9984s-1.7956 3.9984-4.0105 3.9984c-2.215 0-4.0105-1.7901-4.0105-3.9984s1.7956-3.9984 4.0105-3.9984zm0 1.4994c-1.3844 0-2.5066 1.1188-2.5066 2.499s1.1222 2.499 2.5066 2.499 2.5066-1.1188 2.5066-2.499-1.1222-2.499-2.5066-2.499zm0-5.0026c4.6257 0 8.6188 3.1487 9.7267 7.5613 0.10085 0.40165-0.14399 0.80877-0.54686 0.90931-0.40288 0.10054-0.81122-0.14355-0.91208-0.54521-0.94136-3.7492-4.3361-6.4261-8.2678-6.4261-3.9334 0-7.3292 2.6792-8.2689 6.4306-0.10063 0.40171-0.50884 0.64603-0.91177 0.54571s-0.648-0.5073-0.54737-0.90901c1.106-4.4152 5.1003-7.5667 9.728-7.5667z m0.21967 0.21965c-0.26627 0.26627-0.29047 0.68293-0.07262 0.97654l0.07262 0.08412 4.0346 4.0346c-1.922 1.3495-3.3585 3.365-3.9554 5.7495-0.10058 0.4018 0.14362 0.8091 0.54543 0.9097 0.40182 0.1005 0.80909-0.1436 0.90968-0.5455 0.52947-2.1151 1.8371-3.8891 3.5802-5.0341l1.8096 1.8098c-0.70751 0.7215-1.1438 1.71-1.1438 2.8003 0 2.2092 1.7909 4 4 4 1.0904 0 2.0788-0.4363 2.8004-1.1438l5.9193 5.9195c0.2929 0.2929 0.7677 0.2929 1.0606 0 0.2663-0.2662 0.2905-0.6829 0.0726-0.9765l-0.0726-0.0841-6.1135-6.1142 0.0012-0.0015-1.2001-1.1979-2.8699-2.8693 2e-3 -8e-4 -2.8812-2.8782 0.0012-0.0018-1.1333-1.1305-4.3064-4.3058c-0.29289-0.29289-0.76777-0.29289-1.0607 0zm7.9844 9.0458 3.5351 3.5351c-0.45 0.4358-1.0633 0.704-1.7392 0.704-1.3807 0-2.5-1.1193-2.5-2.5 0-0.6759 0.26824-1.2892 0.7041-1.7391zm1.7959-5.7655c-1.0003 0-1.9709 0.14807-2.8889 0.425l1.237 1.2362c0.5358-0.10587 1.0883-0.16119 1.6519-0.16119 3.9231 0 7.3099 2.6803 8.2471 6.4332 0.1004 0.4018 0.5075 0.6462 0.9094 0.5459 0.4019-0.1004 0.6463-0.5075 0.5459-0.9094-1.103-4.417-5.0869-7.5697-9.7024-7.5697zm0.1947 3.5093 3.8013 3.8007c-0.1018-2.0569-1.7488-3.7024-3.8013-3.8007z - + - + 16 @@ -45,10 +43,13 @@ Focusable="True" TemplatedControl.IsTemplateFocusTarget="True"> + + + + + + _window.Dispose(); + + class PropertyReadResult(IntPtr data, IntPtr actualTypeAtom, int actualFormat, IntPtr nItems) + : IDisposable + { + public IntPtr Data => data; + public IntPtr ActualTypeAtom => actualTypeAtom; + public int ActualFormat => actualFormat; + public IntPtr NItems => nItems; + + public void Dispose() + { + XFree(Data); + } + } + + private async Task + WaitForSelectionNotifyAndGetProperty(IntPtr property) + { + var ev = await _window.WaitForEventAsync(ev => + ev.type == XEventName.SelectionNotify + && ev.SelectionEvent.selection == _x11.Atoms.CLIPBOARD + && ev.SelectionEvent.property == property + ); + + if (ev == null) + return null; + + var sel = ev.Value.SelectionEvent; + + return ReadProperty(sel.property); + } + + private PropertyReadResult ReadProperty(IntPtr property) + { + XGetWindowProperty(_x11.Display, _window.Handle, property, IntPtr.Zero, new IntPtr (0x7fffffff), true, + (IntPtr)Atom.AnyPropertyType, + out var actualTypeAtom, out var actualFormat, out var nitems, out var bytes_after, out var prop); + return new (prop, actualTypeAtom, actualFormat, nitems); + } + + private Task ConvertSelectionAndGetProperty( + IntPtr target, IntPtr property) + { + XConvertSelection(_platform.Display, _x11.Atoms.CLIPBOARD, target, property, _window.Handle, + IntPtr.Zero); + return WaitForSelectionNotifyAndGetProperty(property); + } + + public async Task SendFormatRequest() + { + using var res = await ConvertSelectionAndGetProperty(_x11.Atoms.TARGETS, _x11.Atoms.TARGETS); + if (res == null) + return null; + + if (res.NItems == IntPtr.Zero) + return null; + if (res.ActualFormat != 32) + return null; + else + { + var formats = new IntPtr[res.NItems.ToInt32()]; + Marshal.Copy(res.Data, formats, 0, formats.Length); + return formats; + } + } + + public class GetDataResult(byte[]? data, MemoryStream? stream, IntPtr actualTypeAtom) + { + public IntPtr TypeAtom => actualTypeAtom; + public byte[] AsBytes() => data ?? stream!.ToArray(); + public MemoryStream AsStream() => stream ?? new MemoryStream(data!); + } + + private async Task ReadIncr(IntPtr property) + { + XFlush(_platform.Display); + var ms = new MemoryStream(); + void Append(PropertyReadResult res) + { + var len = (int)res.NItems * (res.ActualFormat / 8); + var data = ArrayPool.Shared.Rent(len); + Marshal.Copy(res.Data, data, 0, len); + ms.Write(data, 0, len); + ArrayPool.Shared.Return(data); + } + IntPtr actualTypeAtom = IntPtr.Zero; + while (true) + { + var ev = await _window.WaitForEventAsync(x => + x is { type: XEventName.PropertyNotify, PropertyEvent.state: 0 } && + x.PropertyEvent.atom == property); + + if (ev == null) + return null; + + using var part = ReadProperty(property); + + if (actualTypeAtom == IntPtr.Zero) + actualTypeAtom = part.ActualTypeAtom; + if(part.NItems == IntPtr.Zero) + break; + + Append(part); + } + + return new(null, ms, actualTypeAtom); + } + + public async Task SendDataRequest(IntPtr format) + { + using var res = await ConvertSelectionAndGetProperty(format, format); + if (res == null) + return null; + + if (res.NItems == IntPtr.Zero) + return null; + if (res.ActualTypeAtom == _x11.Atoms.INCR) + { + return await ReadIncr(format); + } + else + { + var data = new byte[(int)res.NItems * (res.ActualFormat / 8)]; + Marshal.Copy(res.Data, data, 0, data.Length); + return new (data, null, res.ActualTypeAtom); + } + + } +} \ No newline at end of file diff --git a/src/Avalonia.X11/Clipboard/EventStreamWindow.cs b/src/Avalonia.X11/Clipboard/EventStreamWindow.cs new file mode 100644 index 0000000000..913e5ed258 --- /dev/null +++ b/src/Avalonia.X11/Clipboard/EventStreamWindow.cs @@ -0,0 +1,110 @@ +using System; +using System.Collections.Generic; +using System.Diagnostics; +using System.Linq; +using System.Threading.Tasks; +using Avalonia.Threading; + +namespace Avalonia.X11; + +internal class EventStreamWindow : IDisposable +{ + private readonly AvaloniaX11Platform _platform; + private IntPtr _handle; + public IntPtr Handle => _handle; + private readonly List<(Func filter, TaskCompletionSource tcs, TimeSpan timeout)> _listeners = new(); + // We are adding listeners to an intermediate collection to avoid freshly added listeners to be called + // in the same event loop iteration and potentially processing an event that was not meant for them. + private readonly List<(Func filter, TaskCompletionSource tcs, TimeSpan timeout)> _addedListeners = new(); + private readonly DispatcherTimer _timeoutTimer; + private readonly bool _isForeign; + private static readonly Stopwatch _time = Stopwatch.StartNew(); + + public EventStreamWindow(AvaloniaX11Platform platform, IntPtr? foreignWindow = null) + { + _platform = platform; + if (foreignWindow.HasValue) + { + _isForeign = true; + _handle = foreignWindow.Value; + _platform.Windows[_handle] = OnEvent; + } + else + _handle = XLib.CreateEventWindow(platform, OnEvent); + + _timeoutTimer = new(TimeSpan.FromSeconds(1), DispatcherPriority.Background, OnTimer); + } + + void MergeListeners() + { + _listeners.AddRange(_addedListeners); + _addedListeners.Clear(); + } + + private void OnTimer(object? sender, EventArgs eventArgs) + { + MergeListeners(); + for (var i = 0; i < _listeners.Count; i++) + { + var (filter, tcs, timeout) = _listeners[i]; + if (timeout < _time.Elapsed) + { + _listeners.RemoveAt(i); + i--; + tcs.SetResult(null); + } + } + if(_listeners.Count == 0) + _timeoutTimer.Stop(); + } + + private void OnEvent(ref XEvent xev) + { + MergeListeners(); + for (var i = 0; i < _listeners.Count; i++) + { + var (filter, tcs, timeout) = _listeners[i]; + if (filter(xev)) + { + _listeners.RemoveAt(i); + i--; + tcs.SetResult(xev); + } + } + } + + public Task WaitForEventAsync(Func predicate, TimeSpan? timeout = null) + { + timeout ??= TimeSpan.FromSeconds(5); + + if (timeout < TimeSpan.Zero) + throw new TimeoutException(); + if(timeout > TimeSpan.FromDays(1)) + throw new ArgumentOutOfRangeException(nameof(timeout)); + + var tcs = new TaskCompletionSource(); + _addedListeners.Add((predicate, tcs, _time.Elapsed + timeout.Value)); + + _timeoutTimer.Start(); + return tcs.Task; + } + + public void Dispose() + { + _timeoutTimer.Stop(); + + _platform.Windows.Remove(_handle); + if (_isForeign) + XLib.XSelectInput(_platform.Display, _handle, IntPtr.Zero); + else + XLib.XDestroyWindow(_platform.Display, _handle); + + _handle = IntPtr.Zero; + var toDispose = _listeners.ToList(); + toDispose.AddRange(_addedListeners); + _listeners.Clear(); + _addedListeners.Clear(); + foreach(var l in toDispose) + l.tcs.SetResult(null); + } +} diff --git a/src/Avalonia.X11/X11Clipboard.cs b/src/Avalonia.X11/Clipboard/X11Clipboard.cs similarity index 67% rename from src/Avalonia.X11/X11Clipboard.cs rename to src/Avalonia.X11/Clipboard/X11Clipboard.cs index 3dea3f812d..75f224126a 100644 --- a/src/Avalonia.X11/X11Clipboard.cs +++ b/src/Avalonia.X11/Clipboard/X11Clipboard.cs @@ -1,27 +1,31 @@ using System; +using System.Buffers; using System.Collections.Generic; +using System.IO; using System.Linq; using System.Runtime.InteropServices; using System.Text; using System.Threading.Tasks; using Avalonia.Input; using Avalonia.Input.Platform; +using Avalonia.X11.Clipboard; using static Avalonia.X11.XLib; namespace Avalonia.X11 { internal class X11Clipboard : IClipboard { + private readonly AvaloniaX11Platform _platform; private readonly X11Info _x11; private IDataObject? _storedDataObject; private IntPtr _handle; private TaskCompletionSource? _storeAtomTcs; - private TaskCompletionSource? _requestedFormatsTcs; - private TaskCompletionSource? _requestedDataTcs; private readonly IntPtr[] _textAtoms; private readonly IntPtr _avaloniaSaveTargetsAtom; + private int _maximumPropertySize; public X11Clipboard(AvaloniaX11Platform platform) { + _platform = platform; _x11 = platform.Info; _handle = CreateEventWindow(platform, OnEvent); _avaloniaSaveTargetsAtom = XInternAtom(_x11.Display, "AVALONIA_SAVE_TARGETS_PROPERTY_ATOM", false); @@ -32,13 +36,15 @@ namespace Avalonia.X11 _x11.Atoms.UTF8_STRING, _x11.Atoms.UTF16_STRING }.Where(a => a != IntPtr.Zero).ToArray(); - } - private bool IsStringAtom(IntPtr atom) - { - return _textAtoms.Contains(atom); + var extendedMaxRequestSize = XExtendedMaxRequestSize(_platform.Display); + var maxRequestSize = XMaxRequestSize(_platform.Display); + _maximumPropertySize = + (int)Math.Min(0x100000, (extendedMaxRequestSize == IntPtr.Zero + ? maxRequestSize + : extendedMaxRequestSize).ToInt64() - 0x100); } - + private Encoding? GetStringEncoding(IntPtr atom) { return (atom == _x11.Atoms.XA_STRING @@ -50,17 +56,17 @@ namespace Avalonia.X11 ? Encoding.Unicode : null; } - + private unsafe void OnEvent(ref XEvent ev) { if (ev.type == XEventName.SelectionClear) - { + { _storeAtomTcs?.TrySetResult(true); return; } if (ev.type == XEventName.SelectionRequest) - { + { var sel = ev.SelectionRequestEvent; var resp = new XEvent { @@ -80,7 +86,7 @@ namespace Avalonia.X11 { resp.SelectionEvent.property = WriteTargetToProperty(sel.target, sel.requestor, sel.property); } - + XSendEvent(_x11.Display, sel.requestor, false, new IntPtr((int)EventMask.NoEventMask), ref resp); } @@ -94,21 +100,19 @@ namespace Avalonia.X11 _x11.Atoms.XA_ATOM, 32, PropertyMode.Replace, atoms, atoms.Length); return property; } - else if(target == _x11.Atoms.SAVE_TARGETS && _x11.Atoms.SAVE_TARGETS != IntPtr.Zero) + else if (target == _x11.Atoms.SAVE_TARGETS && _x11.Atoms.SAVE_TARGETS != IntPtr.Zero) { return property; } - else if ((textEnc = GetStringEncoding(target)) != null + else if ((textEnc = GetStringEncoding(target)) != null && _storedDataObject?.Contains(DataFormats.Text) == true) { var text = _storedDataObject.GetText(); - if(text == null) + if (text == null) return IntPtr.Zero; var data = textEnc.GetBytes(text); - fixed (void* pdata = data) - XChangeProperty(_x11.Display, window, property, target, 8, - PropertyMode.Replace, - pdata, data.Length); + SendDataToClient(window, property, target, data); + return property; } else if (target == _x11.Atoms.MULTIPLE && _x11.Atoms.MULTIPLE != IntPtr.Zero) @@ -136,11 +140,12 @@ namespace Avalonia.X11 return property; } - else if(_x11.Atoms.GetAtomName(target) is { } atomName && _storedDataObject?.Contains(atomName) == true) + else if (_x11.Atoms.GetAtomName(target) is { } atomName && + _storedDataObject?.Contains(atomName) == true) { var objValue = _storedDataObject.Get(atomName); - - if(!(objValue is byte[] bytes)) + + if (!(objValue is byte[] bytes)) { if (objValue is string s) bytes = Encoding.UTF8.GetBytes(s); @@ -148,93 +153,66 @@ namespace Avalonia.X11 return IntPtr.Zero; } - XChangeProperty(_x11.Display, window, property, target, 8, - PropertyMode.Replace, - bytes, bytes.Length); + SendDataToClient(window, property, target, bytes); return property; } else return IntPtr.Zero; } - if (ev.type == XEventName.SelectionNotify && ev.SelectionEvent.selection == _x11.Atoms.CLIPBOARD) - { - var sel = ev.SelectionEvent; - if (sel.property == IntPtr.Zero) - { - _requestedFormatsTcs?.TrySetResult(null); - _requestedDataTcs?.TrySetResult(null); - } - XGetWindowProperty(_x11.Display, _handle, sel.property, IntPtr.Zero, new IntPtr (0x7fffffff), true, (IntPtr)Atom.AnyPropertyType, - out var actualTypeAtom, out var actualFormat, out var nitems, out var bytes_after, out var prop); - Encoding? textEnc; - if (nitems == IntPtr.Zero) - { - _requestedFormatsTcs?.TrySetResult(null); - _requestedDataTcs?.TrySetResult(null); - } - else - { - if (sel.property == _x11.Atoms.TARGETS) - { - if (actualFormat != 32) - _requestedFormatsTcs?.TrySetResult(null); - else - { - var formats = new IntPtr[nitems.ToInt32()]; - Marshal.Copy(prop, formats, 0, formats.Length); - _requestedFormatsTcs?.TrySetResult(formats); - } - } - else if ((textEnc = GetStringEncoding(actualTypeAtom)) != null) - { - var text = textEnc.GetString((byte*)prop.ToPointer(), nitems.ToInt32()); - _requestedDataTcs?.TrySetResult(text); - } - else - { - if (actualTypeAtom == _x11.Atoms.INCR) - { - // TODO: Actually implement that monstrosity - _requestedDataTcs?.TrySetResult(null); - } - else - { - var data = new byte[(int)nitems * (actualFormat / 8)]; - Marshal.Copy(prop, data, 0, data.Length); - _requestedDataTcs?.TrySetResult(data); - } - } - } - - XFree(prop); - } } - private Task SendFormatRequest() + async void SendIncrDataToClient(IntPtr window, IntPtr property, IntPtr target, Stream data) { - if (_requestedFormatsTcs == null || _requestedFormatsTcs.Task.IsCompleted) - _requestedFormatsTcs = new TaskCompletionSource(); - XConvertSelection(_x11.Display, _x11.Atoms.CLIPBOARD, _x11.Atoms.TARGETS, _x11.Atoms.TARGETS, _handle, - IntPtr.Zero); - return _requestedFormatsTcs.Task; + data.Position = 0; + using var events = new EventStreamWindow(_platform, window); + using var _ = data; + XSelectInput(_x11.Display, window, new IntPtr((int)XEventMask.PropertyChangeMask)); + var size = new IntPtr(data.Length); + XChangeProperty(_x11.Display, window, property, _x11.Atoms.INCR, 32, PropertyMode.Replace, ref size, 1); + var buffer = ArrayPool.Shared.Rent((int)Math.Min(_maximumPropertySize, data.Length)); + while (true) + { + if (null == await events.WaitForEventAsync(x => + x.type == XEventName.PropertyNotify && x.PropertyEvent.atom == property + && x.PropertyEvent.state == 1, TimeSpan.FromMinutes(1))) + break; + var read = await data.ReadAsync(buffer, 0, buffer.Length); + if(read == 0) + break; + XChangeProperty(_x11.Display, window, property, target, 8, PropertyMode.Replace, buffer, read); + } + ArrayPool.Shared.Return(buffer); + + // Finish the transfer + XChangeProperty(_x11.Display, window, property, target, 8, PropertyMode.Replace, IntPtr.Zero, 0); } - private Task SendDataRequest(IntPtr format) + void SendDataToClient(IntPtr window, IntPtr property, IntPtr target, byte[] bytes) { - if (_requestedDataTcs == null || _requestedDataTcs.Task.IsCompleted) - _requestedDataTcs = new TaskCompletionSource(); - XConvertSelection(_x11.Display, _x11.Atoms.CLIPBOARD, format, format, _handle, IntPtr.Zero); - return _requestedDataTcs.Task; + if (bytes.Length < _maximumPropertySize) + { + XChangeProperty(_x11.Display, window, property, target, 8, + PropertyMode.Replace, + bytes, bytes.Length); + } + else + SendIncrDataToClient(window, property, target, new MemoryStream(bytes)); } private bool HasOwner => XGetSelectionOwner(_x11.Display, _x11.Atoms.CLIPBOARD) != IntPtr.Zero; + private ClipboardReadSession OpenReadSession() => new(_platform); + public async Task GetTextAsync() { if (!HasOwner) return null; - var res = await SendFormatRequest(); + if (TryGetInProcessDataObject() is { } inProc) + return inProc.GetText(); + + using var session = OpenReadSession(); + var res = await session.SendFormatRequest(); var target = _x11.Atoms.UTF8_STRING; if (res != null) { @@ -247,7 +225,17 @@ namespace Avalonia.X11 } } - return (string?)await SendDataRequest(target); + return ConvertData(await session.SendDataRequest(target)) as string; + } + + private object? ConvertData(ClipboardReadSession.GetDataResult? result) + { + if (result == null) + return null; + if (GetStringEncoding(result.TypeAtom) is { } textEncoding) + return textEncoding.GetString(result.AsBytes()); + // TODO: image encoding + return result.AsBytes(); } @@ -272,6 +260,12 @@ namespace Avalonia.X11 private Task StoreAtomsInClipboardManager(IDataObject data) { + // Skip storing atoms if the data object contains any non-trivial formats or trivial formats are too big + if (data.GetDataFormats().Any(f => f != DataFormats.Text) + || data.GetText()?.Length * 2 > 64 * 1024 + ) + return Task.CompletedTask; + if (_x11.Atoms.CLIPBOARD_MANAGER != IntPtr.Zero && _x11.Atoms.SAVE_TARGETS != IntPtr.Zero) { var clipboardManager = XGetSelectionOwner(_x11.Display, _x11.Atoms.CLIPBOARD_MANAGER); @@ -304,7 +298,7 @@ namespace Avalonia.X11 public Task ClearAsync() { - return SetTextAsync(null); + return SetTextAsync(string.Empty); } public Task SetDataObjectAsync(IDataObject data) @@ -314,19 +308,24 @@ namespace Avalonia.X11 return StoreAtomsInClipboardManager(data); } - public Task TryGetInProcessDataObjectAsync() + private IDataObject? TryGetInProcessDataObject() { if (XGetSelectionOwner(_x11.Display, _x11.Atoms.CLIPBOARD) == _handle) - return Task.FromResult(_storedDataObject); - return Task.FromResult(null); + return _storedDataObject; + return null; } + public Task TryGetInProcessDataObjectAsync() => Task.FromResult(TryGetInProcessDataObject()); + public async Task GetFormatsAsync() { if (!HasOwner) return []; - - var res = await SendFormatRequest(); + if (TryGetInProcessDataObject() is { } inProc) + return inProc.GetDataFormats().ToArray(); + + using var session = OpenReadSession(); + var res = await session.SendFormatRequest(); if (res == null) return []; @@ -347,15 +346,20 @@ namespace Avalonia.X11 { if (!HasOwner) return null; + + if(TryGetInProcessDataObject() is {} inProc) + return inProc.Get(format); + if (format == DataFormats.Text) return await GetTextAsync(); var formatAtom = _x11.Atoms.GetAtom(format); - var res = await SendFormatRequest(); + using var session = OpenReadSession(); + var res = await session.SendFormatRequest(); if (res is null || !res.Contains(formatAtom)) return null; - - return await SendDataRequest(formatAtom); + + return ConvertData(await session.SendDataRequest(formatAtom)); } /// diff --git a/src/Avalonia.X11/Dispatching/GLibDispatcherImpl.cs b/src/Avalonia.X11/Dispatching/GLibDispatcherImpl.cs index 1757b04877..f3a73940fe 100644 --- a/src/Avalonia.X11/Dispatching/GLibDispatcherImpl.cs +++ b/src/Avalonia.X11/Dispatching/GLibDispatcherImpl.cs @@ -237,7 +237,7 @@ internal class GlibDispatcherImpl : } else { - var externalLogger = _platform.Options.ExterinalGLibMainLoopExceptionLogger; + var externalLogger = _platform.Options.ExternalGLibMainLoopExceptionLogger; if (externalLogger != null) externalLogger.Invoke(e); else diff --git a/src/Avalonia.X11/Screens/X11Screen.Providers.cs b/src/Avalonia.X11/Screens/X11Screen.Providers.cs index 82eb21b287..f516e0f44f 100644 --- a/src/Avalonia.X11/Screens/X11Screen.Providers.cs +++ b/src/Avalonia.X11/Screens/X11Screen.Providers.cs @@ -15,22 +15,21 @@ internal partial class X11Screens // Length of a EDID-Block-Length(128 bytes), XRRGetOutputProperty multiplies offset and length by 4 private const int EDIDStructureLength = 32; - public virtual void Refresh() + public virtual void Refresh(MonitorInfo newInfo) { if (scalingProvider == null) return; - var namePtr = XGetAtomName(x11.Display, info.Name); + var namePtr = XGetAtomName(x11.Display, newInfo.Name); var name = Marshal.PtrToStringAnsi(namePtr); XFree(namePtr); DisplayName = name; - IsPrimary = info.IsPrimary; - Bounds = new PixelRect(info.X, info.Y, info.Width, info.Height); - + IsPrimary = newInfo.IsPrimary; + Bounds = new PixelRect(newInfo.X, newInfo.Y, newInfo.Width, newInfo.Height); Size? pSize = null; - for (int o = 0; o < info.Outputs.Length; o++) + for (int o = 0; o < newInfo.Outputs.Length; o++) { - var outputSize = GetPhysicalMonitorSizeFromEDID(info.Outputs[o]); + var outputSize = GetPhysicalMonitorSizeFromEDID(newInfo.Outputs[o]); if (outputSize != null) { pSize = outputSize; @@ -121,7 +120,7 @@ internal partial class X11Screens PhysicalSize = pixelRect.Size.ToSize(Scaling); UpdateWorkArea(); } - public override void Refresh() + public override void Refresh(MonitorInfo newInfo) { } } @@ -131,6 +130,7 @@ internal partial class X11Screens nint[] ScreenKeys { get; } event Action? Changed; X11Screen CreateScreenFromKey(nint key); + MonitorInfo GetMonitorInfoByKey(nint key); } internal unsafe struct MonitorInfo @@ -224,6 +224,20 @@ internal partial class X11Screens throw new ArgumentOutOfRangeException(nameof(key)); } + + public MonitorInfo GetMonitorInfoByKey(nint key) + { + var infos = MonitorInfos; + for (var i = 0; i < infos.Length; i++) + { + if (infos[i].Name == key) + { + return infos[i]; + } + } + + throw new ArgumentOutOfRangeException(nameof(key)); + } } private class FallbackScreensImpl : IX11RawScreenInfoProvider @@ -251,6 +265,11 @@ internal partial class X11Screens return new FallBackScreen(new PixelRect(0, 0, _geo.width, _geo.height), _info); } + public MonitorInfo GetMonitorInfoByKey(nint key) + { + return default; + } + public nint[] ScreenKeys => new[] { IntPtr.Zero }; } } diff --git a/src/Avalonia.X11/Screens/X11Screens.cs b/src/Avalonia.X11/Screens/X11Screens.cs index b8ff80734c..cd6232b6f2 100644 --- a/src/Avalonia.X11/Screens/X11Screens.cs +++ b/src/Avalonia.X11/Screens/X11Screens.cs @@ -15,7 +15,7 @@ namespace Avalonia.X11.Screens _impl = (info.RandrVersion != null && info.RandrVersion >= new Version(1, 5)) ? new Randr15ScreensImpl(platform) : (IX11RawScreenInfoProvider)new FallbackScreensImpl(platform); - _impl.Changed += () => Changed?.Invoke(); + _impl.Changed += OnChanged; } protected override int GetScreenCount() => _impl.ScreenKeys.Length; @@ -24,6 +24,13 @@ namespace Avalonia.X11.Screens protected override X11Screen CreateScreenFromKey(nint key) => _impl.CreateScreenFromKey(key); - protected override void ScreenChanged(X11Screen screen) => screen.Refresh(); + protected override void ScreenChanged(X11Screen screen) + { + var handle = screen.TryGetPlatformHandle()?.Handle; + if (handle != null) + { + screen.Refresh(_impl.GetMonitorInfoByKey(handle.Value)); + } + } } } diff --git a/src/Avalonia.X11/X11Platform.cs b/src/Avalonia.X11/X11Platform.cs index bebae3f0ae..9fbaa4926e 100644 --- a/src/Avalonia.X11/X11Platform.cs +++ b/src/Avalonia.X11/X11Platform.cs @@ -81,7 +81,7 @@ namespace Avalonia.X11 .Bind().ToConstant(new KeyGestureFormatInfo(new Dictionary() { }, meta: "Super")) .Bind().ToFunc(() => KeyboardDevice) .Bind().ToConstant(new X11CursorFactory(Display)) - .Bind().ToConstant(new X11Clipboard(this)) + .Bind().ToLazy(() => new X11Clipboard(this)) .Bind().ToSingleton() .Bind().ToConstant(new X11IconLoader()) .Bind().ToConstant(new LinuxMountedVolumeInfoProvider()) @@ -384,8 +384,8 @@ namespace Avalonia /// will likely brick GLib machinery since it's not aware of managed Exceptions /// This property allows to inspect such exceptions before they will be ignored /// - public Action? ExterinalGLibMainLoopExceptionLogger { get; set; } - + public Action? ExternalGLibMainLoopExceptionLogger { get; set; } + public X11PlatformOptions() { try diff --git a/src/Avalonia.X11/X11PlatformLifetimeEvents.cs b/src/Avalonia.X11/X11PlatformLifetimeEvents.cs index 84b790ec2e..b6db27d6e0 100644 --- a/src/Avalonia.X11/X11PlatformLifetimeEvents.cs +++ b/src/Avalonia.X11/X11PlatformLifetimeEvents.cs @@ -1,4 +1,4 @@ -using System; +using System; using System.Text; using System.Collections.Concurrent; using System.Runtime.InteropServices; @@ -229,7 +229,10 @@ namespace Avalonia.X11 { if (state is IntPtr smcConn) { - var e = new ShutdownRequestedEventArgs(); + var e = new ShutdownRequestedEventArgs() + { + IsOSShutdown = true, + }; if (_platform.Options.EnableSessionManagement) { diff --git a/src/Avalonia.X11/X11Window.Ime.cs b/src/Avalonia.X11/X11Window.Ime.cs index a5a0df926a..768a79af3d 100644 --- a/src/Avalonia.X11/X11Window.Ime.cs +++ b/src/Avalonia.X11/X11Window.Ime.cs @@ -88,15 +88,26 @@ namespace Avalonia.X11 var x11Key = (X11Key)forwardedKey.KeyVal; var keySymbol = _x11.HasXkb ? GetKeySymbolXkb(x11Key) : GetKeySymbolXCore(x11Key); - ScheduleInput(new RawKeyEventArgs( - _keyboard, - (ulong)_x11.LastActivityTimestamp.ToInt64(), - InputRoot, - forwardedKey.Type, - X11KeyTransform.KeyFromX11Key(x11Key), - (RawInputModifiers)forwardedKey.Modifiers, - PhysicalKey.None, - keySymbol)); + ScheduleInput(forwardedKey.WithText ? + new RawKeyEventArgsWithText( + _keyboard, + (ulong)_x11.LastActivityTimestamp.ToInt64(), + InputRoot, + forwardedKey.Type, + X11KeyTransform.KeyFromX11Key(x11Key), + (RawInputModifiers)forwardedKey.Modifiers, + PhysicalKey.None, + keySymbol, + keySymbol) : + new RawKeyEventArgs( + _keyboard, + (ulong)_x11.LastActivityTimestamp.ToInt64(), + InputRoot, + forwardedKey.Type, + X11KeyTransform.KeyFromX11Key(x11Key), + (RawInputModifiers)forwardedKey.Modifiers, + PhysicalKey.None, + keySymbol)); } private void UpdateImePosition() => _imeControl?.UpdateWindowInfo(_position ?? default, RenderScaling); diff --git a/src/Avalonia.X11/X11Window.cs b/src/Avalonia.X11/X11Window.cs index f1f95ee3d5..09772748f9 100644 --- a/src/Avalonia.X11/X11Window.cs +++ b/src/Avalonia.X11/X11Window.cs @@ -951,11 +951,6 @@ namespace Avalonia.X11 XSyncSetCounter(_x11.Display, _xSyncCounter, _xSyncValue); } } - - public void Invalidate(Rect rect) - { - - } public IInputRoot InputRoot => _inputRoot ?? throw new InvalidOperationException($"{nameof(SetInputRoot)} must have been called"); diff --git a/src/Avalonia.X11/XLib.cs b/src/Avalonia.X11/XLib.cs index cfd3a03c8f..2c8ecf2c94 100644 --- a/src/Avalonia.X11/XLib.cs +++ b/src/Avalonia.X11/XLib.cs @@ -559,6 +559,12 @@ namespace Avalonia.X11 [DllImport(libX11)] public static extern void XFreeEventData(IntPtr display, void* cookie); + [DllImport(libX11)] + public static extern IntPtr XMaxRequestSize(IntPtr display); + + [DllImport(libX11)] + public static extern IntPtr XExtendedMaxRequestSize(IntPtr display); + [DllImport(libX11Randr)] public static extern int XRRQueryExtension (IntPtr dpy, out int event_base_return, diff --git a/src/Browser/Avalonia.Browser/BrowserInsetsManager.cs b/src/Browser/Avalonia.Browser/BrowserInsetsManager.cs index dd34f744c1..4c042c5361 100644 --- a/src/Browser/Avalonia.Browser/BrowserInsetsManager.cs +++ b/src/Browser/Avalonia.Browser/BrowserInsetsManager.cs @@ -18,7 +18,7 @@ namespace Avalonia.Browser } } - public override bool DisplayEdgeToEdge { get; set; } + public override bool DisplayEdgeToEdgePreference { get; set; } public override Thickness SafeAreaPadding { diff --git a/src/Browser/Avalonia.Browser/BrowserTextInputMethod.cs b/src/Browser/Avalonia.Browser/BrowserTextInputMethod.cs index fbcbf15ee5..11722851b7 100644 --- a/src/Browser/Avalonia.Browser/BrowserTextInputMethod.cs +++ b/src/Browser/Avalonia.Browser/BrowserTextInputMethod.cs @@ -29,11 +29,13 @@ internal class BrowserTextInputMethod( if (_client != null) { _client.SurroundingTextChanged -= SurroundingTextChanged; + _client.InputPaneActivationRequested -= InputPaneActivationRequested; } if (client != null) { client.SurroundingTextChanged += SurroundingTextChanged; + client.InputPaneActivationRequested += InputPaneActivationRequested; } InputHelper.ClearInputElement(_inputElement); @@ -42,8 +44,7 @@ internal class BrowserTextInputMethod( if (_client != null) { - InputHelper.ShowElement(_inputElement); - InputHelper.FocusElement(_inputElement); + ShowIme(); var surroundingText = _client.SurroundingText ?? ""; var selection = _client.Selection; @@ -56,6 +57,20 @@ internal class BrowserTextInputMethod( } } + private void InputPaneActivationRequested(object? sender, EventArgs e) + { + if (_client != null) + { + ShowIme(); + } + } + + private void ShowIme() + { + InputHelper.ShowElement(_inputElement); + InputHelper.FocusElement(_inputElement); + } + private void SurroundingTextChanged(object? sender, EventArgs e) { if (_client != null) diff --git a/src/Browser/Avalonia.Browser/Rendering/RenderWorker.cs b/src/Browser/Avalonia.Browser/Rendering/RenderWorker.cs index 96cc2ca1a6..f854e922f8 100644 --- a/src/Browser/Avalonia.Browser/Rendering/RenderWorker.cs +++ b/src/Browser/Avalonia.Browser/Rendering/RenderWorker.cs @@ -59,7 +59,9 @@ internal partial class RenderWorker [DynamicDependency(DynamicallyAccessedMemberTypes.All, "System.Runtime.InteropServices.JavaScript.JSHostImplementation", "System.Runtime.InteropServices.JavaScript")] [UnconditionalSuppressMessage("Trimming", "IL2026", Justification = "Private runtime API")] + [UnconditionalSuppressMessage("Trimming", "IL2036", Justification = "Private runtime API")] [UnconditionalSuppressMessage("Trimming", "IL2075", Justification = "Private runtime API")] + [UnconditionalSuppressMessage("Trimming", "IL2111", Justification = "Private runtime API")] static JSWebWorkerClone() { var syncContext = typeof(System.Runtime.InteropServices.JavaScript.JSHost) @@ -107,13 +109,15 @@ internal partial class RenderWorker } // TODO: Use this class instead of JSWebWorkerClone once https://github.com/dotnet/runtime/issues/102010 is fixed + // TODO12: It was fixed in .NET 10 class JSWebWorkerWrapper { [DynamicDependency(DynamicallyAccessedMemberTypes.PublicMethods, "System.Runtime.InteropServices.JavaScript.JSWebWorker", "System.Runtime.InteropServices.JavaScript")] - [UnconditionalSuppressMessage("Trimming", - "IL2026:Members annotated with 'RequiresUnreferencedCodeAttribute' require dynamic access otherwise can break functionality when trimming application code", - Justification = "Private runtime API")] + [UnconditionalSuppressMessage("Trimming", "IL2026", Justification = "Private runtime API")] + [UnconditionalSuppressMessage("Trimming", "IL2036", Justification = "Private runtime API")] + [UnconditionalSuppressMessage("Trimming", "IL2075", Justification = "Private runtime API")] + [UnconditionalSuppressMessage("Trimming", "IL2111", Justification = "Private runtime API")] static JSWebWorkerWrapper() { var type = typeof(System.Runtime.InteropServices.JavaScript.JSHost) diff --git a/src/Browser/Avalonia.Browser/build/Avalonia.Browser.props b/src/Browser/Avalonia.Browser/build/Avalonia.Browser.props index 80a1741cac..d8e74f437f 100644 --- a/src/Browser/Avalonia.Browser/build/Avalonia.Browser.props +++ b/src/Browser/Avalonia.Browser/build/Avalonia.Browser.props @@ -7,6 +7,9 @@ true true + + <_AvaloniaRuntimeAssetsLocation>$(WasmRuntimeAssetsLocation) + <_AvaloniaRuntimeAssetsLocation Condition="'$(_AvaloniaRuntimeAssetsLocation)' == ''">_framework diff --git a/src/Browser/Avalonia.Browser/build/Avalonia.Browser.targets b/src/Browser/Avalonia.Browser/build/Avalonia.Browser.targets index 13f2ed78ba..88097cd16c 100644 --- a/src/Browser/Avalonia.Browser/build/Avalonia.Browser.targets +++ b/src/Browser/Avalonia.Browser/build/Avalonia.Browser.targets @@ -13,8 +13,7 @@ - - + diff --git a/src/Browser/Avalonia.Browser/build/Microsoft.AspNetCore.StaticWebAssets.props b/src/Browser/Avalonia.Browser/build/Microsoft.AspNetCore.StaticWebAssets.props index 79d0385074..55d33fd46c 100644 --- a/src/Browser/Avalonia.Browser/build/Microsoft.AspNetCore.StaticWebAssets.props +++ b/src/Browser/Avalonia.Browser/build/Microsoft.AspNetCore.StaticWebAssets.props @@ -10,7 +10,7 @@ Avalonia.Browser $(_AvaloniaWebAssetsFolder) / - $(WasmRuntimeAssetsLocation)/%(FileName)%(Extension) + $(_AvaloniaRuntimeAssetsLocation)/%(FileName)%(Extension) All All Primary diff --git a/src/Headless/Avalonia.Headless/AvaloniaHeadlessPlatform.cs b/src/Headless/Avalonia.Headless/AvaloniaHeadlessPlatform.cs index e85aaca138..75767988d7 100644 --- a/src/Headless/Avalonia.Headless/AvaloniaHeadlessPlatform.cs +++ b/src/Headless/Avalonia.Headless/AvaloniaHeadlessPlatform.cs @@ -62,8 +62,6 @@ namespace Avalonia.Headless public IWindowImpl CreateEmbeddableWindow() => throw new PlatformNotSupportedException(); - public IPopupImpl CreatePopup() => new HeadlessWindowImpl(true, _frameBufferFormat); - public ITrayIconImpl? CreateTrayIcon() => null; } diff --git a/src/Headless/Avalonia.Headless/HeadlessPlatformRenderInterface.cs b/src/Headless/Avalonia.Headless/HeadlessPlatformRenderInterface.cs index d018ef491f..2774bf63fe 100644 --- a/src/Headless/Avalonia.Headless/HeadlessPlatformRenderInterface.cs +++ b/src/Headless/Avalonia.Headless/HeadlessPlatformRenderInterface.cs @@ -23,8 +23,6 @@ namespace Avalonia.Headless .Bind().ToConstant(new HeadlessTextShaperStub()); } - public IEnumerable InstalledFontNames { get; } = new[] { "Tahoma" }; - public IPlatformRenderInterfaceContext CreateBackendContext(IPlatformGraphicsContext? graphicsContext) => this; public bool SupportsIndividualRoundRects => false; @@ -452,8 +450,6 @@ namespace Avalonia.Headless public Matrix Transform { get; set; } - public RenderOptions RenderOptions { get; set; } - public void Clear(Color color) { @@ -517,16 +513,6 @@ namespace Avalonia.Headless } - public void PushBitmapBlendMode(BitmapBlendingMode blendingMode) - { - - } - - public void PopBitmapBlendMode() - { - - } - public object? GetFeature(Type t) { return null; @@ -540,10 +526,6 @@ namespace Avalonia.Headless { } - public void DrawRectangle(IPen pen, Rect rect, float cornerRadius = 0) - { - } - public void DrawBitmap(IBitmapImpl source, double opacity, Rect sourceRect, Rect destRect) { diff --git a/src/Headless/Avalonia.Headless/HeadlessPlatformStubs.cs b/src/Headless/Avalonia.Headless/HeadlessPlatformStubs.cs index 893bb7ec95..4595df43a9 100644 --- a/src/Headless/Avalonia.Headless/HeadlessPlatformStubs.cs +++ b/src/Headless/Avalonia.Headless/HeadlessPlatformStubs.cs @@ -245,7 +245,11 @@ namespace Avalonia.Headless public virtual bool TryCreateGlyphTypeface(Stream stream, FontSimulations fontSimulations, out IGlyphTypeface glyphTypeface) { - glyphTypeface = new HeadlessGlyphTypefaceImpl(FontFamily.DefaultFontFamilyName, FontStyle.Normal, FontWeight.Normal, FontStretch.Normal); + glyphTypeface = new HeadlessGlyphTypefaceImpl( + FontFamily.DefaultFontFamilyName, + fontSimulations.HasFlag(FontSimulations.Oblique) ? FontStyle.Italic : FontStyle.Normal, + fontSimulations.HasFlag(FontSimulations.Bold) ? FontWeight.Bold : FontWeight.Normal, + FontStretch.Normal); TryCreateGlyphTypefaceCount++; diff --git a/src/Headless/Avalonia.Headless/HeadlessWindowImpl.cs b/src/Headless/Avalonia.Headless/HeadlessWindowImpl.cs index 7366d8235d..64ce9945d4 100644 --- a/src/Headless/Avalonia.Headless/HeadlessWindowImpl.cs +++ b/src/Headless/Avalonia.Headless/HeadlessWindowImpl.cs @@ -59,10 +59,6 @@ namespace Avalonia.Headless public Compositor Compositor => AvaloniaHeadlessPlatform.Compositor!; - public void Invalidate(Rect rect) - { - } - public void SetInputRoot(IInputRoot inputRoot) { InputRoot = inputRoot; @@ -96,16 +92,6 @@ namespace Avalonia.Headless Dispatcher.UIThread.Post(() => Deactivated?.Invoke(), DispatcherPriority.Input); } - public void BeginMoveDrag() - { - - } - - public void BeginResizeDrag(WindowEdge edge) - { - - } - public PixelPoint Position { get; set; } public Action? PositionChanged { get; set; } public void Activate() diff --git a/src/Markup/Avalonia.Markup.Xaml.Loader/CompilerExtensions/AvaloniaXamlIlCompiler.cs b/src/Markup/Avalonia.Markup.Xaml.Loader/CompilerExtensions/AvaloniaXamlIlCompiler.cs index 4fec398c95..e0225a24f7 100644 --- a/src/Markup/Avalonia.Markup.Xaml.Loader/CompilerExtensions/AvaloniaXamlIlCompiler.cs +++ b/src/Markup/Avalonia.Markup.Xaml.Loader/CompilerExtensions/AvaloniaXamlIlCompiler.cs @@ -60,7 +60,6 @@ namespace Avalonia.Markup.Xaml.XamlIl.CompilerExtensions new AvaloniaXamlIlDuplicateSettersChecker(), new AvaloniaXamlIlControlTemplateTargetTypeMetadataTransformer(), new AvaloniaXamlIlBindingPathParser(), - new AvaloniaXamlIlPropertyPathTransformer(), new AvaloniaXamlIlSetterTargetTypeMetadataTransformer(), new AvaloniaXamlIlSetterTransformer(), new AvaloniaXamlIlStyleValidatorTransformer(), diff --git a/src/Markup/Avalonia.Markup.Xaml.Loader/CompilerExtensions/AvaloniaXamlIlLanguageParseIntrinsics.cs b/src/Markup/Avalonia.Markup.Xaml.Loader/CompilerExtensions/AvaloniaXamlIlLanguageParseIntrinsics.cs index 93a4b3146c..69cdec68c8 100644 --- a/src/Markup/Avalonia.Markup.Xaml.Loader/CompilerExtensions/AvaloniaXamlIlLanguageParseIntrinsics.cs +++ b/src/Markup/Avalonia.Markup.Xaml.Loader/CompilerExtensions/AvaloniaXamlIlLanguageParseIntrinsics.cs @@ -421,7 +421,7 @@ namespace Avalonia.Markup.Xaml.XamlIl.CompilerExtensions return ReturnOnParseError($"x:Array element {element.Type.GetClrType().Name} is not assignable to the array element type {elementType.Name}", out result); } } - + if (types.AvaloniaList.MakeGenericType(elementType).IsAssignableFrom(type)) { result = new AvaloniaXamlIlAvaloniaListConstantAstNode(node, types, type, elementType, nodes); @@ -439,11 +439,8 @@ namespace Avalonia.Markup.Xaml.XamlIl.CompilerExtensions result = new AvaloniaXamlIlArrayConstantAstNode(node, listType, elementType, nodes); return true; } - - result = null; - return false; } - + result = null; return false; } diff --git a/src/Markup/Avalonia.Markup.Xaml.Loader/CompilerExtensions/Transformers/AvaloniaXamlIlBindingPathParser.cs b/src/Markup/Avalonia.Markup.Xaml.Loader/CompilerExtensions/Transformers/AvaloniaXamlIlBindingPathParser.cs index 62e81f44d8..c74118b6f7 100644 --- a/src/Markup/Avalonia.Markup.Xaml.Loader/CompilerExtensions/Transformers/AvaloniaXamlIlBindingPathParser.cs +++ b/src/Markup/Avalonia.Markup.Xaml.Loader/CompilerExtensions/Transformers/AvaloniaXamlIlBindingPathParser.cs @@ -22,8 +22,7 @@ namespace Avalonia.Markup.Xaml.XamlIl.CompilerExtensions.Transformers if (binding.Arguments.Count > 0 && binding.Arguments[0] is XamlAstTextNode bindingPathText) { - var reader = new CharacterReader(bindingPathText.Text.AsSpan()); - var (nodes, _) = BindingExpressionGrammar.Parse(ref reader); + var (nodes, _) = BindingExpressionGrammar.Parse(bindingPathText.Text); if (convertedNode != null) { @@ -48,8 +47,7 @@ namespace Avalonia.Markup.Xaml.XamlIl.CompilerExtensions.Transformers if (bindingPathAssignment != null && bindingPathAssignment.Values[0] is XamlAstTextNode pathValue) { - var reader = new CharacterReader(pathValue.Text.AsSpan()); - var (nodes, _) = BindingExpressionGrammar.Parse(ref reader); + var (nodes, _) = BindingExpressionGrammar.Parse(pathValue.Text); if (nodes.Count == 1 && nodes[0] is BindingExpressionGrammar.EmptyExpressionNode) { diff --git a/src/Markup/Avalonia.Markup.Xaml.Loader/CompilerExtensions/Transformers/AvaloniaXamlIlPropertyPathTransformer.cs b/src/Markup/Avalonia.Markup.Xaml.Loader/CompilerExtensions/Transformers/AvaloniaXamlIlPropertyPathTransformer.cs deleted file mode 100644 index 27413b386a..0000000000 --- a/src/Markup/Avalonia.Markup.Xaml.Loader/CompilerExtensions/Transformers/AvaloniaXamlIlPropertyPathTransformer.cs +++ /dev/null @@ -1,262 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using Avalonia.Markup.Parsers; -using XamlX; -using XamlX.Ast; -using XamlX.Transform; -using XamlX.Transform.Transformers; -using XamlX.TypeSystem; -using XamlX.Emit; -using XamlX.IL; - -namespace Avalonia.Markup.Xaml.XamlIl.CompilerExtensions.Transformers -{ - class XamlPropertyPathException : XamlTransformException - { - public XamlPropertyPathException(string message, IXamlLineInfo lineInfo, Exception? innerException = null) - : base(message, lineInfo, innerException) - { - } - } - - class AvaloniaXamlIlPropertyPathTransformer : IXamlAstTransformer - { - public IXamlAstNode Transform(AstTransformationContext context, IXamlAstNode node) - { - if (node is XamlAstXamlPropertyValueNode pv - && pv.Values.Count == 1 - && pv.Values[0] is XamlAstTextNode text - && pv.Property.GetClrProperty().Getter?.ReturnType - .Equals(context.GetAvaloniaTypes().PropertyPath) == true - ) - { - var parentScope = context.ParentNodes().OfType() - .FirstOrDefault(); - if(parentScope == null) - throw new XamlPropertyPathException("No target type scope found for property path", text); - if (parentScope.ScopeType != AvaloniaXamlIlTargetTypeMetadataNode.ScopeTypes.Style) - throw new XamlPropertyPathException("PropertyPath is currently only valid for styles", pv); - - - IEnumerable parsed; - try - { - parsed = PropertyPathGrammar.Parse(text.Text); - } - catch (Exception e) - { - throw new XamlPropertyPathException("Unable to parse PropertyPath: " + e.Message, text, innerException: e); - } - - var elements = new List(); - var currentType = parentScope.TargetType.GetClrType(); - - - var expectProperty = true; - var expectCast = true; - var expectTraversal = false; - var types = context.GetAvaloniaTypes(); - - IXamlType GetType(string? ns, string name) - { - return TypeReferenceResolver.ResolveType(context, $"{ns}:{name}", false, - text, true).GetClrType(); - } - - void HandleProperty(string name, string? typeNamespace, string? typeName) - { - if(!expectProperty || currentType == null) - throw new XamlPropertyPathException("Unexpected property node", text); - - var propertySearchType = - typeName != null ? GetType(typeNamespace, typeName) : currentType; - - IXamlIlPropertyPathElementNode? prop = null; - var avaloniaPropertyFieldName = name + "Property"; - var avaloniaPropertyField = propertySearchType.GetAllFields().FirstOrDefault(f => - f.IsStatic && f.IsPublic && f.Name == avaloniaPropertyFieldName); - if (avaloniaPropertyField != null) - { - prop = new XamlIlAvaloniaPropertyPropertyPathElementNode(avaloniaPropertyField, - XamlIlAvaloniaPropertyHelper.GetAvaloniaPropertyType(avaloniaPropertyField, types, text)); - } - else - { - var clrProperty = propertySearchType.GetAllProperties().FirstOrDefault(p => p.Name == name); - if (clrProperty is not null) - prop = new XamlIClrPropertyPathElementNode(clrProperty); - } - - if (prop == null) - throw new XamlPropertyPathException( - $"Unable to resolve property {name} on type {propertySearchType.GetFqn()}", - text); - - currentType = prop.Type; - elements.Add(prop); - expectProperty = false; - expectTraversal = expectCast = true; - } - - foreach (var ge in parsed) - { - if (ge is PropertyPathGrammar.ChildTraversalSyntax) - { - if (!expectTraversal) - throw new XamlPropertyPathException("Unexpected child traversal .", text); - elements.Add(new XamlIlChildTraversalPropertyPathElementNode()); - expectTraversal = expectCast = false; - expectProperty = true; - } - else if (ge is PropertyPathGrammar.EnsureTypeSyntax ets) - { - if(!expectCast) - throw new XamlPropertyPathException("Unexpected cast node", text); - currentType = GetType(ets.TypeNamespace, ets.TypeName); - elements.Add(new XamlIlCastPropertyPathElementNode(currentType, true)); - expectProperty = false; - expectCast = expectTraversal = true; - } - else if (ge is PropertyPathGrammar.CastTypeSyntax cts) - { - if(!expectCast) - throw new XamlPropertyPathException("Unexpected cast node", text); - //TODO: Check if cast can be done - currentType = GetType(cts.TypeNamespace, cts.TypeName); - elements.Add(new XamlIlCastPropertyPathElementNode(currentType, false)); - expectProperty = false; - expectCast = expectTraversal = true; - } - else if (ge is PropertyPathGrammar.PropertySyntax ps) - { - HandleProperty(ps.Name, null, null); - } - else if (ge is PropertyPathGrammar.TypeQualifiedPropertySyntax tqps) - { - HandleProperty(tqps.Name, tqps.TypeNamespace, tqps.TypeName); - } - else - throw new XamlPropertyPathException("Unexpected node " + ge, text); - - } - var propertyPathNode = new XamlIlPropertyPathNode(text, elements, types); - if (propertyPathNode.Type == null) - throw new XamlPropertyPathException("Unexpected end of the property path", text); - pv.Values[0] = propertyPathNode; - } - - return node; - } - - interface IXamlIlPropertyPathElementNode - { - void Emit(XamlEmitContext context, IXamlILEmitter codeGen); - IXamlType? Type { get; } - } - - class XamlIlChildTraversalPropertyPathElementNode : IXamlIlPropertyPathElementNode - { - public void Emit(XamlEmitContext context, IXamlILEmitter codeGen) - => codeGen.EmitCall( - context.GetAvaloniaTypes() - .PropertyPathBuilder.GetMethod(m => m.Name == "ChildTraversal")); - - public IXamlType? Type => null; - } - - class XamlIlAvaloniaPropertyPropertyPathElementNode : IXamlIlPropertyPathElementNode - { - private readonly IXamlField _field; - - public XamlIlAvaloniaPropertyPropertyPathElementNode(IXamlField field, IXamlType propertyType) - { - _field = field; - Type = propertyType; - } - - public void Emit(XamlEmitContext context, IXamlILEmitter codeGen) - => codeGen - .Ldsfld(_field) - .EmitCall(context.GetAvaloniaTypes() - .PropertyPathBuilder.GetMethod(m => m.Name == "Property")); - - public IXamlType Type { get; } - } - - class XamlIClrPropertyPathElementNode : IXamlIlPropertyPathElementNode - { - private readonly IXamlProperty _property; - - public XamlIClrPropertyPathElementNode(IXamlProperty property) - { - _property = property; - } - - public void Emit(XamlEmitContext context, IXamlILEmitter codeGen) - { - context.Configuration.GetExtra() - .Emit(context, codeGen, _property); - - codeGen.EmitCall(context.GetAvaloniaTypes() - .PropertyPathBuilder.GetMethod(m => m.Name == "Property")); - } - - public IXamlType Type => _property.PropertyType; - } - - class XamlIlCastPropertyPathElementNode : IXamlIlPropertyPathElementNode - { - private readonly IXamlType _type; - private readonly bool _ensureType; - - public XamlIlCastPropertyPathElementNode(IXamlType type, bool ensureType) - { - _type = type; - _ensureType = ensureType; - } - - public void Emit(XamlEmitContext context, IXamlILEmitter codeGen) - { - codeGen - .Ldtype(_type) - .EmitCall(context.GetAvaloniaTypes() - .PropertyPathBuilder.GetMethod(m => m.Name == (_ensureType ? "EnsureType" : "Cast"))); - } - - public IXamlType Type => _type; - } - - class XamlIlPropertyPathNode : XamlAstNode, IXamlIlPropertyPathNode, IXamlAstEmitableNode - { - private readonly List _elements; - private readonly AvaloniaXamlIlWellKnownTypes _types; - - public XamlIlPropertyPathNode(IXamlLineInfo lineInfo, - List elements, - AvaloniaXamlIlWellKnownTypes types) : base(lineInfo) - { - _elements = elements; - _types = types; - Type = new XamlAstClrTypeReference(this, types.PropertyPath, false); - } - - public IXamlAstTypeReference Type { get; } - public IXamlType? PropertyType => _elements.LastOrDefault()?.Type; - public XamlILNodeEmitResult Emit(XamlEmitContext context, IXamlILEmitter codeGen) - { - codeGen - .Newobj(_types.PropertyPathBuilder.GetConstructor()); - foreach(var e in _elements) - e.Emit(context, codeGen); - codeGen.EmitCall(_types.PropertyPathBuilder.GetMethod(m => m.Name == "Build")); - return XamlILNodeEmitResult.Type(0, _types.PropertyPath); - } - } - } - - interface IXamlIlPropertyPathNode : IXamlAstValueNode - { - IXamlType? PropertyType { get; } - } -} diff --git a/src/Markup/Avalonia.Markup.Xaml.Loader/CompilerExtensions/Transformers/AvaloniaXamlIlSetterTransformer.cs b/src/Markup/Avalonia.Markup.Xaml.Loader/CompilerExtensions/Transformers/AvaloniaXamlIlSetterTransformer.cs index 68a5321bf2..86812240df 100644 --- a/src/Markup/Avalonia.Markup.Xaml.Loader/CompilerExtensions/Transformers/AvaloniaXamlIlSetterTransformer.cs +++ b/src/Markup/Avalonia.Markup.Xaml.Loader/CompilerExtensions/Transformers/AvaloniaXamlIlSetterTransformer.cs @@ -77,9 +77,6 @@ namespace Avalonia.Markup.Xaml.XamlIl.CompilerExtensions.Transformers .FirstOrDefault(x => x.Property.GetClrProperty().Name == "PropertyPath"); if (propertyPath == null) throw new XamlStyleTransformException("Setter without a property or property path is not valid", node); - if (propertyPath.Values[0] is IXamlIlPropertyPathNode ppn - && ppn.PropertyType != null) - propType = ppn.PropertyType; else throw new XamlStyleTransformException("Unable to get the property path property type", node); } diff --git a/src/Markup/Avalonia.Markup.Xaml.Loader/CompilerExtensions/Transformers/AvaloniaXamlIlWellKnownTypes.cs b/src/Markup/Avalonia.Markup.Xaml.Loader/CompilerExtensions/Transformers/AvaloniaXamlIlWellKnownTypes.cs index 32c09b5cc7..cab4716aa3 100644 --- a/src/Markup/Avalonia.Markup.Xaml.Loader/CompilerExtensions/Transformers/AvaloniaXamlIlWellKnownTypes.cs +++ b/src/Markup/Avalonia.Markup.Xaml.Loader/CompilerExtensions/Transformers/AvaloniaXamlIlWellKnownTypes.cs @@ -49,8 +49,6 @@ namespace Avalonia.Markup.Xaml.XamlIl.CompilerExtensions.Transformers public IXamlMethod INameScopeComplete { get; } public IXamlType IPropertyInfo { get; } public IXamlType ClrPropertyInfo { get; } - public IXamlType PropertyPath { get; } - public IXamlType PropertyPathBuilder { get; } public IXamlType IPropertyAccessor { get; } public IXamlType PropertyInfoAccessorFactory { get; } public IXamlType CompiledBindingPathBuilder { get; } @@ -240,8 +238,6 @@ namespace Avalonia.Markup.Xaml.XamlIl.CompilerExtensions.Transformers false, AvaloniaProperty, XamlIlTypes.Object, BindingPriority); IPropertyInfo = cfg.TypeSystem.GetType("Avalonia.Data.Core.IPropertyInfo"); ClrPropertyInfo = cfg.TypeSystem.GetType("Avalonia.Data.Core.ClrPropertyInfo"); - PropertyPath = cfg.TypeSystem.GetType("Avalonia.Data.Core.PropertyPath"); - PropertyPathBuilder = cfg.TypeSystem.GetType("Avalonia.Data.Core.PropertyPathBuilder"); IPropertyAccessor = cfg.TypeSystem.GetType("Avalonia.Data.Core.Plugins.IPropertyAccessor"); PropertyInfoAccessorFactory = cfg.TypeSystem.GetType("Avalonia.Markup.Xaml.MarkupExtensions.CompiledBindings.PropertyInfoAccessorFactory"); CompiledBindingPathBuilder = cfg.TypeSystem.GetType("Avalonia.Markup.Xaml.MarkupExtensions.CompiledBindings.CompiledBindingPathBuilder"); diff --git a/src/Markup/Avalonia.Markup.Xaml.Loader/CompilerExtensions/Transformers/XamlPropertyPathException.cs b/src/Markup/Avalonia.Markup.Xaml.Loader/CompilerExtensions/Transformers/XamlPropertyPathException.cs new file mode 100644 index 0000000000..52e10aa4cc --- /dev/null +++ b/src/Markup/Avalonia.Markup.Xaml.Loader/CompilerExtensions/Transformers/XamlPropertyPathException.cs @@ -0,0 +1,14 @@ +using System; +using XamlX; +using XamlX.Ast; + +namespace Avalonia.Markup.Xaml.XamlIl.CompilerExtensions.Transformers +{ + internal class XamlPropertyPathException : XamlTransformException + { + public XamlPropertyPathException(string message, IXamlLineInfo lineInfo, Exception? innerException = null) + : base(message, lineInfo, innerException) + { + } + } +} diff --git a/src/Markup/Avalonia.Markup.Xaml.Loader/CompilerExtensions/XamlIlAvaloniaPropertyHelper.cs b/src/Markup/Avalonia.Markup.Xaml.Loader/CompilerExtensions/XamlIlAvaloniaPropertyHelper.cs index 0d4b6eceef..48d7f26a5e 100644 --- a/src/Markup/Avalonia.Markup.Xaml.Loader/CompilerExtensions/XamlIlAvaloniaPropertyHelper.cs +++ b/src/Markup/Avalonia.Markup.Xaml.Loader/CompilerExtensions/XamlIlAvaloniaPropertyHelper.cs @@ -64,7 +64,7 @@ namespace Avalonia.Markup.Xaml.XamlIl.CompilerExtensions { XamlAstNamePropertyReference forgedReference; - var parsedPropertyName = PropertyParser.Parse(new CharacterReader(propertyName.AsSpan())); + var parsedPropertyName = PropertyParser.Parse(propertyName); if(parsedPropertyName.owner == null) forgedReference = new XamlAstNamePropertyReference(lineInfo, selectorTypeReference, propertyName, selectorTypeReference); diff --git a/src/Markup/Avalonia.Markup.Xaml/Parsers/PropertyParser.cs b/src/Markup/Avalonia.Markup.Xaml/Parsers/PropertyParser.cs index a55a44dfe5..0f12307046 100644 --- a/src/Markup/Avalonia.Markup.Xaml/Parsers/PropertyParser.cs +++ b/src/Markup/Avalonia.Markup.Xaml/Parsers/PropertyParser.cs @@ -1,10 +1,17 @@ -using Avalonia.Data.Core; +using System; +using Avalonia.Data.Core; using Avalonia.Utilities; namespace Avalonia.Markup.Xaml.Parsers { internal class PropertyParser { + public static (string? ns, string? owner, string name) Parse(string text) + { + var r = new CharacterReader(text.AsSpan()); + return Parse(r); + } + public static (string? ns, string? owner, string name) Parse(CharacterReader r) { if (r.End) diff --git a/src/Markup/Avalonia.Markup/Markup/Parsers/BindingExpressionGrammar.cs b/src/Markup/Avalonia.Markup/Markup/Parsers/BindingExpressionGrammar.cs index 2529950691..97ae40931e 100644 --- a/src/Markup/Avalonia.Markup/Markup/Parsers/BindingExpressionGrammar.cs +++ b/src/Markup/Avalonia.Markup/Markup/Parsers/BindingExpressionGrammar.cs @@ -1,10 +1,10 @@ // Copyright (c) The Avalonia Project. All rights reserved. // Licensed under the MIT license. See licence.md file in the project root for full license information. -using Avalonia.Data.Core; -using Avalonia.Utilities; using System; using System.Collections.Generic; +using Avalonia.Data.Core; +using Avalonia.Utilities; namespace Avalonia.Markup.Parsers { @@ -18,6 +18,12 @@ namespace Avalonia.Markup.Parsers { private static readonly List s_pool = new(); + public static (List Nodes, SourceMode Mode) Parse(string text) + { + var r = new CharacterReader(text.AsSpan()); + return Parse(ref r); + } + public static (List Nodes, SourceMode Mode) Parse(ref CharacterReader r) { var result = new List(); diff --git a/src/Skia/Avalonia.Skia/Avalonia.Skia.csproj b/src/Skia/Avalonia.Skia/Avalonia.Skia.csproj index f30056c8d9..d1f77823e2 100644 --- a/src/Skia/Avalonia.Skia/Avalonia.Skia.csproj +++ b/src/Skia/Avalonia.Skia/Avalonia.Skia.csproj @@ -4,6 +4,8 @@ true true true + + $(WarningsAsErrors);CS0618 diff --git a/src/Skia/Avalonia.Skia/DrawingContextImpl.cs b/src/Skia/Avalonia.Skia/DrawingContextImpl.cs index ce5b59a86d..70d6e2c10f 100644 --- a/src/Skia/Avalonia.Skia/DrawingContextImpl.cs +++ b/src/Skia/Avalonia.Skia/DrawingContextImpl.cs @@ -246,12 +246,12 @@ namespace Avalonia.Skia var d = destRect.ToSKRect(); var paint = SKPaintCache.Shared.Get(); + var samplingOptions = RenderOptions.BitmapInterpolationMode.ToSKSamplingOptions(); paint.Color = new SKColor(255, 255, 255, (byte)(255 * opacity * _currentOpacity)); - paint.FilterQuality = RenderOptions.BitmapInterpolationMode.ToSKFilterQuality(); paint.BlendMode = RenderOptions.BitmapBlendingMode.ToSKBlendMode(); - drawableImage.Draw(this, s, d, paint); + drawableImage.Draw(this, s, d, samplingOptions, paint); SKPaintCache.Shared.ReturnReset(paint); } @@ -844,7 +844,9 @@ namespace Avalonia.Skia /// public Matrix Transform { - get { return _currentTransform ??= Canvas.TotalMatrix.ToAvaloniaMatrix(); } + // There is a Canvas.TotalMatrix (non 4x4 overload), but internally it still uses 4x4 matrix. + // We want to avoid SKMatrix4x4 -> SKMatrix -> Matrix conversion by directly going SKMatrix4x4 -> Matrix. + get { return _currentTransform ??= Canvas.TotalMatrix44.ToAvaloniaMatrix(); } set { CheckLease(); @@ -860,7 +862,9 @@ namespace Avalonia.Skia transform *= _postTransform.Value; } - Canvas.SetMatrix(transform.ToSKMatrix()); + // Canvas.SetMatrix internally uses 4x4 matrix, even with SKMatrix(3x3) overload. + // We want to avoid Matrix -> SKMatrix -> SKMatrix4x4 conversion by directly going Matrix -> SKMatrix4x4. + Canvas.SetMatrix(transform.ToSKMatrix44()); } } @@ -1257,7 +1261,6 @@ namespace Avalonia.Skia using(var shader = tile.ToShader(tileX, tileY, shaderTransform.ToSKMatrix(), new SKRect(0, 0, tile.CullRect.Width, tile.CullRect.Height))) { - paintWrapper.Paint.FilterQuality = SKFilterQuality.None; paintWrapper.Paint.Shader = shader; } } @@ -1304,18 +1307,6 @@ namespace Avalonia.Skia return (byte)(r * 255); } - private static Color Blend(Color left, Color right) - { - var aa = left.A / 255d; - var ab = right.A / 255d; - return new Color( - (byte)((aa + ab * (1 - aa)) * 255), - Blend(left.R, left.A, right.R, right.A), - Blend(left.G, left.A, right.G, right.A), - Blend(left.B, left.A, right.B, right.A) - ); - } - internal PaintWrapper CreateAcrylicPaint (SKPaint paint, IExperimentalAcrylicMaterial material) { var paintWrapper = new PaintWrapper(paint); @@ -1535,16 +1526,6 @@ namespace Avalonia.Skia _disposable3 = null; } - public IDisposable ApplyTo(SKPaint paint) - { - var state = new PaintState(paint, paint.Color, paint.Shader); - - paint.Color = Paint.Color; - paint.Shader = Paint.Shader; - - return state; - } - /// /// Add new disposable to a wrapper. /// diff --git a/src/Skia/Avalonia.Skia/Gpu/Metal/SkiaMetalApi.cs b/src/Skia/Avalonia.Skia/Gpu/Metal/SkiaMetalApi.cs deleted file mode 100644 index d5e2352e13..0000000000 --- a/src/Skia/Avalonia.Skia/Gpu/Metal/SkiaMetalApi.cs +++ /dev/null @@ -1,108 +0,0 @@ -using System; -using System.Diagnostics.CodeAnalysis; -using System.Reflection; -using System.Runtime.CompilerServices; -using System.Runtime.InteropServices; -using Avalonia.Compatibility; -using Avalonia.Platform.Interop; -using SkiaSharp; -using BindingFlags = System.Reflection.BindingFlags; - -namespace Avalonia.Skia.Metal; - -internal unsafe class SkiaMetalApi -{ - delegate* unmanaged[Stdcall] _gr_direct_context_make_metal_with_options; - private delegate* unmanaged[Stdcall] - _gr_backendrendertarget_new_metal; - private readonly ConstructorInfo _contextCtor; - private readonly MethodInfo _contextOptionsToNative; - private readonly ConstructorInfo _renderTargetCtor; - - [DynamicDependency(DynamicallyAccessedMemberTypes.NonPublicConstructors, typeof(GRContext))] - [DynamicDependency(DynamicallyAccessedMemberTypes.NonPublicConstructors, typeof(GRBackendRenderTarget))] - [DynamicDependency(DynamicallyAccessedMemberTypes.NonPublicMethods, typeof(GRContextOptions))] - [DynamicDependency(DynamicallyAccessedMemberTypes.All, "SkiaSharp.GRContextOptionsNative", "SkiaSharp")] - public SkiaMetalApi() - { - // Make sure that skia is loaded - GC.KeepAlive(new SKPaint()); - - // https://github.com/mono/SkiaSharp/blob/25e70a390e2128e5a54d28795365bf9fdaa7161c/binding/SkiaSharp/SkiaApi.cs#L9-L13 - // Note, IsIOS also returns true on MacCatalyst. - var libSkiaSharpPath = OperatingSystemEx.IsIOS() || OperatingSystemEx.IsTvOS() ? - "@rpath/libSkiaSharp.framework/libSkiaSharp" : - "libSkiaSharp"; - var dll = NativeLibraryEx.Load(libSkiaSharpPath, typeof(SKPaint).Assembly); - - IntPtr address; - - if (NativeLibraryEx.TryGetExport(dll, "gr_direct_context_make_metal_with_options", out address)) - { - _gr_direct_context_make_metal_with_options = - (delegate* unmanaged[Stdcall] )address; - } - else - { - throw new InvalidOperationException( - "Unable to export gr_direct_context_make_metal_with_options. Make sure SkiaSharp is up to date."); - } - - if(NativeLibraryEx.TryGetExport(dll, "gr_backendrendertarget_new_metal", out address)) - { - _gr_backendrendertarget_new_metal = - (delegate* unmanaged[Stdcall])address; - } - else - { - throw new InvalidOperationException( - "Unable to export gr_backendrendertarget_new_metal. Make sure SkiaSharp is up to date."); - } - - _contextCtor = typeof(GRContext).GetConstructor( - BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic, null, - new[] { typeof(IntPtr), typeof(bool) }, null) ?? throw new MissingMemberException("GRContext.ctor(IntPtr,bool)"); - - - _renderTargetCtor = typeof(GRBackendRenderTarget).GetConstructor( - BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic, null, - new[] { typeof(IntPtr), typeof(bool) }, null) ?? throw new MissingMemberException("GRContext.ctor(IntPtr,bool)"); - - _contextOptionsToNative = typeof(GRContextOptions).GetMethod("ToNative", - BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic) - ?? throw new MissingMemberException("GRContextOptions.ToNative()"); - } - - [UnconditionalSuppressMessage("Trimming", "IL3050", Justification = "We have DynamicDependency above.")] - public GRContext CreateContext(IntPtr device, IntPtr queue, GRContextOptions? options) - { - options ??= new(); - var nativeOptions = _contextOptionsToNative.Invoke(options, null)!; - var gcHandle = GCHandle.Alloc(nativeOptions, GCHandleType.Pinned); - try - { - var context = _gr_direct_context_make_metal_with_options(device, queue, gcHandle.AddrOfPinnedObject()); - if (context == IntPtr.Zero) - throw new InvalidOperationException("Unable to create GRContext from Metal device."); - return (GRContext)_contextCtor.Invoke(new object[] { context, true }); - } - finally - { - gcHandle.Free(); - } - } - - internal struct GRMtlTextureInfoNative - { - public IntPtr Texture; - } - - public GRBackendRenderTarget CreateBackendRenderTarget(int width, int height, int samples, IntPtr texture) - { - var info = new GRMtlTextureInfoNative() { Texture = texture }; - var target = _gr_backendrendertarget_new_metal(width, height, samples, &info); - if (target == IntPtr.Zero) - throw new InvalidOperationException("Unable to create GRBackendRenderTarget"); - return (GRBackendRenderTarget)_renderTargetCtor.Invoke(new object[] { target, true }); - } -} diff --git a/src/Skia/Avalonia.Skia/Gpu/Metal/SkiaMetalGpu.cs b/src/Skia/Avalonia.Skia/Gpu/Metal/SkiaMetalGpu.cs index bfd6e1aa59..8faa754ef8 100644 --- a/src/Skia/Avalonia.Skia/Gpu/Metal/SkiaMetalGpu.cs +++ b/src/Skia/Avalonia.Skia/Gpu/Metal/SkiaMetalGpu.cs @@ -9,14 +9,15 @@ namespace Avalonia.Skia.Metal; internal class SkiaMetalGpu : ISkiaGpu, ISkiaGpuWithPlatformGraphicsContext { - private SkiaMetalApi _api = new(); private GRContext? _context; private readonly IMetalDevice _device; public SkiaMetalGpu(IMetalDevice device, long? maxResourceBytes) { - _context = _api.CreateContext(device.Device, device.CommandQueue, - new GRContextOptions() { AvoidStencilBuffers = true }); + _context = GRContext.CreateMetal( + new GRMtlBackendContext { DeviceHandle = device.Device, QueueHandle = device.CommandQueue, }, + new GRContextOptions { AvoidStencilBuffers = true }) + ?? throw new InvalidOperationException("Unable to create GRContext from Metal device."); _device = device; if (maxResourceBytes.HasValue) _context.SetResourceCacheLimit(maxResourceBytes.Value); @@ -35,7 +36,7 @@ internal class SkiaMetalGpu : ISkiaGpu, ISkiaGpuWithPlatformGraphicsContext public IPlatformGraphicsContext? PlatformGraphicsContext => _device; public IScopedResource TryGetGrContext() => - ScopedResource.Create(_context ?? throw new ObjectDisposedException(nameof(SkiaMetalApi)), + ScopedResource.Create(_context ?? throw new ObjectDisposedException(nameof(SkiaMetalGpu)), EnsureCurrent().Dispose); public ISkiaGpuRenderTarget? TryCreateRenderTarget(IEnumerable surfaces) @@ -72,13 +73,13 @@ internal class SkiaMetalGpu : ISkiaGpu, ISkiaGpuWithPlatformGraphicsContext public ISkiaGpuRenderSession BeginRenderingSession() { var session = (_target ?? throw new ObjectDisposedException(nameof(SkiaMetalRenderTarget))).BeginRendering(); - var backendTarget = _gpu._api.CreateBackendRenderTarget(session.Size.Width, session.Size.Height, - 1, session.Texture); + var backendTarget = new GRBackendRenderTarget(session.Size.Width, session.Size.Height, + new GRMtlTextureInfo(session.Texture)); var surface = SKSurface.Create(_gpu._context!, backendTarget, session.IsYFlipped ? GRSurfaceOrigin.BottomLeft : GRSurfaceOrigin.TopLeft, SKColorType.Bgra8888); - + return new SkiaMetalRenderSession(_gpu, surface, session); } diff --git a/src/Skia/Avalonia.Skia/Gpu/OpenGl/FboSkiaSurface.cs b/src/Skia/Avalonia.Skia/Gpu/OpenGl/FboSkiaSurface.cs index 4a3031d9ad..d9027e24d7 100644 --- a/src/Skia/Avalonia.Skia/Gpu/OpenGl/FboSkiaSurface.cs +++ b/src/Skia/Avalonia.Skia/Gpu/OpenGl/FboSkiaSurface.cs @@ -89,10 +89,10 @@ namespace Avalonia.Skia throw new OpenGlException("Unable to create FBO with stencil"); } - var target = new GRBackendRenderTarget(pixelSize.Width, pixelSize.Height, 0, 8, + using var target = new GRBackendRenderTarget(pixelSize.Width, pixelSize.Height, 0, 8, new GRGlFramebufferInfo((uint)_fbo, SKColorType.Rgba8888.ToGlSizedFormat())); - _surface = SKSurface.Create(_grContext, target, - surfaceOrigin, SKColorType.Rgba8888, new SKSurfaceProperties(SKPixelGeometry.RgbHorizontal)); + using var properties = new SKSurfaceProperties(SKPixelGeometry.RgbHorizontal); + _surface = SKSurface.Create(_grContext, target, surfaceOrigin, SKColorType.Rgba8888, properties); CanBlit = gl.IsBlitFramebufferAvailable; } diff --git a/src/Skia/Avalonia.Skia/Gpu/Vulkan/VulkanSkiaExternalObjectsFeature.cs b/src/Skia/Avalonia.Skia/Gpu/Vulkan/VulkanSkiaExternalObjectsFeature.cs index 5c5427b8d9..16f705e088 100644 --- a/src/Skia/Avalonia.Skia/Gpu/Vulkan/VulkanSkiaExternalObjectsFeature.cs +++ b/src/Skia/Avalonia.Skia/Gpu/Vulkan/VulkanSkiaExternalObjectsFeature.cs @@ -93,7 +93,7 @@ internal class VulkanSkiaExternalObjectsFeature : IExternalObjectsRenderInterfac Size = info.MemorySize } }; - using var renderTarget = new GRBackendRenderTarget(_properties.Width, _properties.Height, 1, imageInfo); + using var renderTarget = new GRBackendRenderTarget(_properties.Width, _properties.Height, imageInfo); using var surface = SKSurface.Create(_gpu.GrContext, renderTarget, _properties.TopLeftOrigin ? GRSurfaceOrigin.TopLeft : GRSurfaceOrigin.BottomLeft, _properties.Format == PlatformGraphicsExternalImageFormat.R8G8B8A8UNorm @@ -121,4 +121,4 @@ internal class VulkanSkiaExternalObjectsFeature : IExternalObjectsRenderInterfac public IReadOnlyList SupportedImageHandleTypes => _feature.SupportedImageHandleTypes; public IReadOnlyList SupportedSemaphoreTypes => _feature.SupportedSemaphoreTypes; -} \ No newline at end of file +} diff --git a/src/Skia/Avalonia.Skia/Gpu/Vulkan/VulkanSkiaRenderTarget.cs b/src/Skia/Avalonia.Skia/Gpu/Vulkan/VulkanSkiaRenderTarget.cs index b761d21b4e..c86a1204f3 100644 --- a/src/Skia/Avalonia.Skia/Gpu/Vulkan/VulkanSkiaRenderTarget.cs +++ b/src/Skia/Avalonia.Skia/Gpu/Vulkan/VulkanSkiaRenderTarget.cs @@ -54,7 +54,7 @@ class VulkanSkiaRenderTarget : ISkiaGpuRenderTarget Size = sessionImageInfo.MemorySize } }; - using var renderTarget = new GRBackendRenderTarget(size.Width, size.Height, 1, imageInfo); + using var renderTarget = new GRBackendRenderTarget(size.Width, size.Height, imageInfo); var surface = SKSurface.Create(_gpu.GrContext, renderTarget, session.IsYFlipped ? GRSurfaceOrigin.TopLeft : GRSurfaceOrigin.BottomLeft, session.IsRgba ? SKColorType.Rgba8888 : SKColorType.Bgra8888, SKColorSpace.CreateSrgb()); diff --git a/src/Skia/Avalonia.Skia/Helpers/PixelFormatHelper.cs b/src/Skia/Avalonia.Skia/Helpers/PixelFormatHelper.cs index 84a1972e01..f1cd39b2a7 100644 --- a/src/Skia/Avalonia.Skia/Helpers/PixelFormatHelper.cs +++ b/src/Skia/Avalonia.Skia/Helpers/PixelFormatHelper.cs @@ -18,11 +18,6 @@ namespace Avalonia.Skia.Helpers { var colorType = format?.ToSkColorType() ?? SKImageInfo.PlatformColorType; - // TODO: This looks like some leftover hack - if (OperatingSystemEx.IsLinux()) - { - colorType = SKColorType.Bgra8888; - } return colorType; } diff --git a/src/Skia/Avalonia.Skia/IDrawableBitmapImpl.cs b/src/Skia/Avalonia.Skia/IDrawableBitmapImpl.cs index be751021d5..9fe6f9d7e3 100644 --- a/src/Skia/Avalonia.Skia/IDrawableBitmapImpl.cs +++ b/src/Skia/Avalonia.Skia/IDrawableBitmapImpl.cs @@ -14,7 +14,8 @@ namespace Avalonia.Skia /// Drawing context. /// Source rect. /// Destination rect. + /// Interpolation sampling options. /// Paint to use. - void Draw(DrawingContextImpl context, SKRect sourceRect, SKRect destRect, SKPaint paint); + void Draw(DrawingContextImpl context, SKRect sourceRect, SKRect destRect, SKSamplingOptions samplingOptions, SKPaint paint); } } diff --git a/src/Skia/Avalonia.Skia/ImmutableBitmap.cs b/src/Skia/Avalonia.Skia/ImmutableBitmap.cs index ec645692b2..c8068fb6fa 100644 --- a/src/Skia/Avalonia.Skia/ImmutableBitmap.cs +++ b/src/Skia/Avalonia.Skia/ImmutableBitmap.cs @@ -50,7 +50,7 @@ namespace Avalonia.Skia { SKImageInfo info = new SKImageInfo(destinationSize.Width, destinationSize.Height, SKColorType.Bgra8888); _bitmap = new SKBitmap(info); - src._image.ScalePixels(_bitmap.PeekPixels(), interpolationMode.ToSKFilterQuality()); + src._image.ScalePixels(_bitmap.PeekPixels(), interpolationMode.ToSKSamplingOptions()); _bitmap.SetImmutable(); _image = SKImage.FromBitmap(_bitmap); @@ -95,11 +95,11 @@ namespace Avalonia.Skia if (_bitmap.Width != desired.Width || _bitmap.Height != desired.Height) { - var scaledBmp = _bitmap.Resize(desired, interpolationMode.ToSKFilterQuality()); + var scaledBmp = _bitmap.Resize(desired, interpolationMode.ToSKSamplingOptions()); _bitmap.Dispose(); _bitmap = scaledBmp; } - + _bitmap.SetImmutable(); _image = SKImage.FromBitmap(_bitmap); @@ -171,9 +171,9 @@ namespace Avalonia.Skia } /// - public void Draw(DrawingContextImpl context, SKRect sourceRect, SKRect destRect, SKPaint paint) + public void Draw(DrawingContextImpl context, SKRect sourceRect, SKRect destRect, SKSamplingOptions samplingOptions, SKPaint paint) { - context.Canvas.DrawImage(_image, sourceRect, destRect, paint); + context.Canvas.DrawImage(_image, sourceRect, destRect, samplingOptions, paint); } public PixelFormat? Format => _bitmap?.ColorType.ToAvalonia(); diff --git a/src/Skia/Avalonia.Skia/SkiaSharpExtensions.cs b/src/Skia/Avalonia.Skia/SkiaSharpExtensions.cs index add72caa30..177dbf5cba 100644 --- a/src/Skia/Avalonia.Skia/SkiaSharpExtensions.cs +++ b/src/Skia/Avalonia.Skia/SkiaSharpExtensions.cs @@ -9,22 +9,20 @@ namespace Avalonia.Skia { public static class SkiaSharpExtensions { - public static SKFilterQuality ToSKFilterQuality(this BitmapInterpolationMode interpolationMode) + public static SKSamplingOptions ToSKSamplingOptions(this BitmapInterpolationMode interpolationMode) { - switch (interpolationMode) + return interpolationMode switch { - case BitmapInterpolationMode.Unspecified: - case BitmapInterpolationMode.LowQuality: - return SKFilterQuality.Low; - case BitmapInterpolationMode.MediumQuality: - return SKFilterQuality.Medium; - case BitmapInterpolationMode.HighQuality: - return SKFilterQuality.High; - case BitmapInterpolationMode.None: - return SKFilterQuality.None; - default: - throw new ArgumentOutOfRangeException(nameof(interpolationMode), interpolationMode, null); - } + BitmapInterpolationMode.None => + new SKSamplingOptions(SKFilterMode.Nearest, SKMipmapMode.None), + BitmapInterpolationMode.Unspecified or BitmapInterpolationMode.LowQuality => + new SKSamplingOptions(SKFilterMode.Linear, SKMipmapMode.None), + BitmapInterpolationMode.MediumQuality => + new SKSamplingOptions(SKFilterMode.Linear, SKMipmapMode.Linear), + BitmapInterpolationMode.HighQuality => + new SKSamplingOptions(SKCubicResampler.Mitchell), + _ => throw new ArgumentOutOfRangeException(nameof(interpolationMode), interpolationMode, null) + }; } public static SKBlendMode ToSKBlendMode(this BitmapBlendingMode blendingMode) @@ -146,11 +144,41 @@ namespace Avalonia.Skia return sm; } + public static SKMatrix44 ToSKMatrix44(this Matrix m) + { + var sm = new SKMatrix44 + { + M00 = (float)m.M11, + M01 = (float)m.M12, + M02 = 0, + M03 = (float)m.M13, + M10 = (float)m.M21, + M11 = (float)m.M22, + M12 = 0, + M13 = (float)m.M23, + M20 = 0, + M21 = 0, + M22 = 1, + M23 = 0, + M30 = (float)m.M31, + M31 = (float)m.M32, + M32 = 0, + M33 = (float)m.M33 + }; + + return sm; + } + internal static Matrix ToAvaloniaMatrix(this SKMatrix m) => new( m.ScaleX, m.SkewY, m.Persp0, m.SkewX, m.ScaleY, m.Persp1, m.TransX, m.TransY, m.Persp2); + internal static Matrix ToAvaloniaMatrix(this SKMatrix44 m) => new( + m.M00, m.M01, m.M03, + m.M10, m.M11, m.M13, + m.M30, m.M31, m.M33); + public static SKColor ToSKColor(this Color c) { return new SKColor(c.R, c.G, c.B, c.A); diff --git a/src/Skia/Avalonia.Skia/SurfaceRenderTarget.cs b/src/Skia/Avalonia.Skia/SurfaceRenderTarget.cs index 27e36d99b3..87aebc2df1 100644 --- a/src/Skia/Avalonia.Skia/SurfaceRenderTarget.cs +++ b/src/Skia/Avalonia.Skia/SurfaceRenderTarget.cs @@ -2,7 +2,6 @@ using System; using System.IO; using Avalonia.Reactive; using Avalonia.Platform; -using Avalonia.Rendering; using Avalonia.Skia.Helpers; using SkiaSharp; @@ -164,12 +163,12 @@ namespace Avalonia.Skia public bool CanBlit => true; /// - public void Draw(DrawingContextImpl context, SKRect sourceRect, SKRect destRect, SKPaint paint) + public void Draw(DrawingContextImpl context, SKRect sourceRect, SKRect destRect, SKSamplingOptions samplingOptions, SKPaint paint) { using var image = SnapshotImage(); - context.Canvas.DrawImage(image, sourceRect, destRect, paint); + context.Canvas.DrawImage(image, sourceRect, destRect, samplingOptions, paint); } - + /// /// Create Skia image snapshot from a surface. /// diff --git a/src/Skia/Avalonia.Skia/TextShaperImpl.cs b/src/Skia/Avalonia.Skia/TextShaperImpl.cs index cedb2f63cf..efce67e90b 100644 --- a/src/Skia/Avalonia.Skia/TextShaperImpl.cs +++ b/src/Skia/Avalonia.Skia/TextShaperImpl.cs @@ -14,6 +14,9 @@ namespace Avalonia.Skia { internal class TextShaperImpl : ITextShaperImpl { + [ThreadStatic] + private static Buffer? s_buffer; + private static readonly ConcurrentDictionary s_cachedLanguage = new(); public ShapedBuffer ShapeText(ReadOnlyMemory text, TextShaperOptions options) @@ -24,69 +27,70 @@ namespace Avalonia.Skia var bidiLevel = options.BidiLevel; var culture = options.Culture; - using (var buffer = new Buffer()) - { - // HarfBuzz needs the surrounding characters to correctly shape the text - var containingText = GetContainingMemory(text, out var start, out var length).Span; - buffer.AddUtf16(containingText, start, length); + var buffer = s_buffer ??= new Buffer(); - MergeBreakPair(buffer); + buffer.Reset(); - buffer.GuessSegmentProperties(); + // HarfBuzz needs the surrounding characters to correctly shape the text + var containingText = GetContainingMemory(text, out var start, out var length).Span; + buffer.AddUtf16(containingText, start, length); - buffer.Direction = (bidiLevel & 1) == 0 ? Direction.LeftToRight : Direction.RightToLeft; + MergeBreakPair(buffer); - var usedCulture = culture ?? CultureInfo.CurrentCulture; + buffer.GuessSegmentProperties(); - buffer.Language = s_cachedLanguage.GetOrAdd(usedCulture.LCID, _ => new Language(usedCulture)); + buffer.Direction = (bidiLevel & 1) == 0 ? Direction.LeftToRight : Direction.RightToLeft; - var font = ((GlyphTypefaceImpl)typeface).Font; + var usedCulture = culture ?? CultureInfo.CurrentCulture; - font.Shape(buffer, GetFeatures(options)); + buffer.Language = s_cachedLanguage.GetOrAdd(usedCulture.LCID, _ => new Language(usedCulture)); - if (buffer.Direction == Direction.RightToLeft) - { - buffer.Reverse(); - } + var font = ((GlyphTypefaceImpl)typeface).Font; - font.GetScale(out var scaleX, out _); + font.Shape(buffer, GetFeatures(options)); - var textScale = fontRenderingEmSize / scaleX; + if (buffer.Direction == Direction.RightToLeft) + { + buffer.Reverse(); + } - var bufferLength = buffer.Length; + font.GetScale(out var scaleX, out _); - var shapedBuffer = new ShapedBuffer(text, bufferLength, typeface, fontRenderingEmSize, bidiLevel); + var textScale = fontRenderingEmSize / scaleX; - var glyphInfos = buffer.GetGlyphInfoSpan(); + var bufferLength = buffer.Length; - var glyphPositions = buffer.GetGlyphPositionSpan(); + var shapedBuffer = new ShapedBuffer(text, bufferLength, typeface, fontRenderingEmSize, bidiLevel); - for (var i = 0; i < bufferLength; i++) - { - var sourceInfo = glyphInfos[i]; + var glyphInfos = buffer.GetGlyphInfoSpan(); - var glyphIndex = (ushort)sourceInfo.Codepoint; + var glyphPositions = buffer.GetGlyphPositionSpan(); - var glyphCluster = (int)sourceInfo.Cluster; + for (var i = 0; i < bufferLength; i++) + { + var sourceInfo = glyphInfos[i]; - var glyphAdvance = GetGlyphAdvance(glyphPositions, i, textScale) + options.LetterSpacing; + var glyphIndex = (ushort)sourceInfo.Codepoint; - var glyphOffset = GetGlyphOffset(glyphPositions, i, textScale); + var glyphCluster = (int)sourceInfo.Cluster; - if (glyphCluster < containingText.Length && containingText[glyphCluster] == '\t') - { - glyphIndex = typeface.GetGlyph(' '); + var glyphAdvance = GetGlyphAdvance(glyphPositions, i, textScale) + options.LetterSpacing; - glyphAdvance = options.IncrementalTabWidth > 0 ? - options.IncrementalTabWidth : - 4 * typeface.GetGlyphAdvance(glyphIndex) * textScale; - } + var glyphOffset = GetGlyphOffset(glyphPositions, i, textScale); - shapedBuffer[i] = new Media.TextFormatting.GlyphInfo(glyphIndex, glyphCluster, glyphAdvance, glyphOffset); + if (glyphCluster < containingText.Length && containingText[glyphCluster] == '\t') + { + glyphIndex = typeface.GetGlyph(' '); + + glyphAdvance = options.IncrementalTabWidth > 0 ? + options.IncrementalTabWidth : + 4 * typeface.GetGlyphAdvance(glyphIndex) * textScale; } - return shapedBuffer; + shapedBuffer[i] = new Media.TextFormatting.GlyphInfo(glyphIndex, glyphCluster, glyphAdvance, glyphOffset); } + + return shapedBuffer; } private static void MergeBreakPair(Buffer buffer) diff --git a/src/Skia/Avalonia.Skia/WriteableBitmapImpl.cs b/src/Skia/Avalonia.Skia/WriteableBitmapImpl.cs index 313c7e06de..387b84046e 100644 --- a/src/Skia/Avalonia.Skia/WriteableBitmapImpl.cs +++ b/src/Skia/Avalonia.Skia/WriteableBitmapImpl.cs @@ -73,7 +73,7 @@ namespace Avalonia.Skia if (bmp.Width != desired.Width || bmp.Height != desired.Height) { - var scaledBmp = bmp.Resize(desired, interpolationMode.ToSKFilterQuality()); + var scaledBmp = bmp.Resize(desired, interpolationMode.ToSKSamplingOptions()); bmp.Dispose(); bmp = scaledBmp; } @@ -118,7 +118,7 @@ namespace Avalonia.Skia public int Version { get; private set; } = 1; /// - public void Draw(DrawingContextImpl context, SKRect sourceRect, SKRect destRect, SKPaint paint) + public void Draw(DrawingContextImpl context, SKRect sourceRect, SKRect destRect, SKSamplingOptions samplingOptions, SKPaint paint) { lock (_lock) { @@ -132,7 +132,7 @@ namespace Avalonia.Skia _image = GetSnapshot(); _imageValid = true; } - context.Canvas.DrawImage(_image, sourceRect, destRect, paint); + context.Canvas.DrawImage(_image, sourceRect, destRect, samplingOptions, paint); } } diff --git a/src/Tizen/Avalonia.Tizen/NuiAvaloniaViewTextEditable.cs b/src/Tizen/Avalonia.Tizen/NuiAvaloniaViewTextEditable.cs index c3e7577674..c7e05afc52 100644 --- a/src/Tizen/Avalonia.Tizen/NuiAvaloniaViewTextEditable.cs +++ b/src/Tizen/Avalonia.Tizen/NuiAvaloniaViewTextEditable.cs @@ -118,6 +118,7 @@ internal class NuiAvaloniaViewTextEditable client.TextViewVisualChanged += OnTextViewVisualChanged; client.SurroundingTextChanged += OnSurroundingTextChanged; client.SelectionChanged += OnClientSelectionChanged; + client.InputPaneActivationRequested += OnInputPaneActivationRequested; TextInput.SelectWholeText(); OnClientSelectionChanged(this, EventArgs.Empty); @@ -125,6 +126,12 @@ internal class NuiAvaloniaViewTextEditable finally { _updating = false; } } + private void OnInputPaneActivationRequested(object? sender, EventArgs e) + { + var inputContext = TextInput.GetInputMethodContext(); + inputContext.ShowInputPanel(); + } + private void OnClientSelectionChanged(object? sender, EventArgs e) => InvokeUpdate(client => { if (client.Selection.End == 0 || client.Selection.Start == client.Selection.End) @@ -152,6 +159,7 @@ internal class NuiAvaloniaViewTextEditable _client!.TextViewVisualChanged -= OnTextViewVisualChanged; _client!.SurroundingTextChanged -= OnSurroundingTextChanged; _client!.SelectionChanged -= OnClientSelectionChanged; + _client!.InputPaneActivationRequested -= OnInputPaneActivationRequested; } if (Window.Instance.GetDefaultLayer().Children.Contains((View)TextInput)) diff --git a/src/Windows/Avalonia.Win32/Avalonia.Win32.csproj b/src/Windows/Avalonia.Win32/Avalonia.Win32.csproj index e26a4cd69f..8836f307d9 100644 --- a/src/Windows/Avalonia.Win32/Avalonia.Win32.csproj +++ b/src/Windows/Avalonia.Win32/Avalonia.Win32.csproj @@ -18,7 +18,7 @@ - + diff --git a/src/Windows/Avalonia.Win32/DComposition/dcomp.idl b/src/Windows/Avalonia.Win32/DComposition/dcomp.idl index ebc0fba608..173239c4c0 100644 --- a/src/Windows/Avalonia.Win32/DComposition/dcomp.idl +++ b/src/Windows/Avalonia.Win32/DComposition/dcomp.idl @@ -21,7 +21,7 @@ @clr-map INT16 short @clr-map INT32 int @clr-map INT64 long -@clr-map UINT ushort +@clr-map UINT uint @clr-map UINT16 ushort @clr-map ULONG uint @clr-map UINT32 uint diff --git a/src/Windows/Avalonia.Win32/DirectX/DirectXEnums.cs b/src/Windows/Avalonia.Win32/DirectX/DirectXEnums.cs index 1357e8aa63..ac6f0678d7 100644 --- a/src/Windows/Avalonia.Win32/DirectX/DirectXEnums.cs +++ b/src/Windows/Avalonia.Win32/DirectX/DirectXEnums.cs @@ -213,6 +213,14 @@ namespace Avalonia.Win32.DirectX DXGI_ERROR_WAS_STILL_DRAWING = 0x887A000A } + [Flags] + internal enum DXGI_MWA : uint + { + DXGI_MWA_NO_WINDOW_CHANGES = 1, + DXGI_MWA_NO_ALT_ENTER = 2, + DXGI_MWA_NO_PRINT_SCREEN = 4 + } + internal static class DxgiErrorExtensions { public static bool IsDeviceLostError(this DXGI_ERROR error) diff --git a/src/Windows/Avalonia.Win32/DirectX/DxgiRenderTarget.cs b/src/Windows/Avalonia.Win32/DirectX/DxgiRenderTarget.cs index 513aba4f20..c7306ce86e 100644 --- a/src/Windows/Avalonia.Win32/DirectX/DxgiRenderTarget.cs +++ b/src/Windows/Avalonia.Win32/DirectX/DxgiRenderTarget.cs @@ -23,7 +23,7 @@ namespace Avalonia.Win32.DirectX private IUnknown? _renderTexture; private RECT _clientRect; - + public DxgiRenderTarget(EglGlPlatformSurface.IEglWindowGlPlatformSurfaceInfo window, EglContext context, DxgiConnection connection) : base(context) { _window = window; @@ -68,6 +68,8 @@ namespace Avalonia.Win32.DirectX null ); + _dxgiFactory.MakeWindowAssociation(window.Handle, (uint)(DXGI_MWA.DXGI_MWA_NO_ALT_ENTER | DXGI_MWA.DXGI_MWA_NO_PRINT_SCREEN)); + GetClientRect(_window.Handle, out var pClientRect); _clientRect = pClientRect; } diff --git a/src/Windows/Avalonia.Win32/DirectX/directx.idl b/src/Windows/Avalonia.Win32/DirectX/directx.idl index 2ad75d18f3..5cf2b5959f 100644 --- a/src/Windows/Avalonia.Win32/DirectX/directx.idl +++ b/src/Windows/Avalonia.Win32/DirectX/directx.idl @@ -19,7 +19,7 @@ @clr-map INT16 short @clr-map INT32 int @clr-map INT64 long -@clr-map UINT ushort +@clr-map UINT uint @clr-map UINT16 ushort @clr-map ULONG uint @clr-map UINT32 uint diff --git a/src/Windows/Avalonia.Win32/Interop/UnmanagedMethods.cs b/src/Windows/Avalonia.Win32/Interop/UnmanagedMethods.cs index ed5c7ba85c..1177334680 100644 --- a/src/Windows/Avalonia.Win32/Interop/UnmanagedMethods.cs +++ b/src/Windows/Avalonia.Win32/Interop/UnmanagedMethods.cs @@ -1502,6 +1502,21 @@ namespace Avalonia.Win32.Interop [DllImport("shell32", CharSet = CharSet.Auto)] public static extern int Shell_NotifyIcon(NIM dwMessage, NOTIFYICONDATA lpData); + [DllImport("user32.dll", SetLastError = true)] + public static extern bool ChangeWindowMessageFilterEx( + IntPtr hWnd, + uint message, + MessageFilterFlag action, + IntPtr pChangeFilterStruct + ); + + public enum MessageFilterFlag + { + MSGFLT_RESET = 0, + MSGFLT_ALLOW = 1, + MSGFLT_DISALLOW = 2, + } + [DllImport("shell32", CharSet = CharSet.Auto)] public static extern nint SHAppBarMessage(AppBarMessage dwMessage, ref APPBARDATA lpData); diff --git a/src/Windows/Avalonia.Win32/TrayIconImpl.cs b/src/Windows/Avalonia.Win32/TrayIconImpl.cs index 0199e1bc74..fdb6327a85 100644 --- a/src/Windows/Avalonia.Win32/TrayIconImpl.cs +++ b/src/Windows/Avalonia.Win32/TrayIconImpl.cs @@ -37,7 +37,12 @@ namespace Avalonia.Win32 new PixelSize(32, 32), new Vector(96, 96), PixelFormats.Bgra8888, AlphaFormat.Unpremul); s_emptyIcon = new Win32Icon(bitmap); } - + + internal static void ChangeWindowMessageFilter(IntPtr hWnd) + { + ChangeWindowMessageFilterEx(hWnd, WM_TASKBARCREATED, MessageFilterFlag.MSGFLT_ALLOW, IntPtr.Zero); + } + public TrayIconImpl() { FindTaskBarMonitor(); diff --git a/src/Windows/Avalonia.Win32/Win32Com/win32.idl b/src/Windows/Avalonia.Win32/Win32Com/win32.idl index 34dc746133..a0abefe1b6 100644 --- a/src/Windows/Avalonia.Win32/Win32Com/win32.idl +++ b/src/Windows/Avalonia.Win32/Win32Com/win32.idl @@ -17,7 +17,7 @@ @clr-map INT16 short @clr-map INT32 int @clr-map INT64 long -@clr-map UINT ushort +@clr-map UINT uint @clr-map UINT16 ushort @clr-map ULONG uint @clr-map UINT32 uint diff --git a/src/Windows/Avalonia.Win32/Win32Platform.cs b/src/Windows/Avalonia.Win32/Win32Platform.cs index e9df0d04cc..1cf30137d2 100644 --- a/src/Windows/Avalonia.Win32/Win32Platform.cs +++ b/src/Windows/Avalonia.Win32/Win32Platform.cs @@ -1,4 +1,4 @@ -using System; +using System; using System.ComponentModel; using System.Diagnostics.CodeAnalysis; using System.IO; @@ -148,11 +148,22 @@ namespace Avalonia.Win32 { if (ShutdownRequested != null) { - var e = new ShutdownRequestedEventArgs(); + // https://learn.microsoft.com/en-us/windows/win32/shutdown/wm-queryendsession + // > LPARAM lParam // logoff option + // > + // > This parameter can be one or more of the following values. If this parameter is 0, the system is shutting down or restarting (it is not possible to determine which event is occurring). + // > + // > - ENDSESSION_CLOSEAPP 0x00000001 The application is using a file that must be replaced, the system is being serviced, or system resources are exhausted. For more information, see Guidelines for Applications. + // > - ENDSESSION_CRITICAL 0x40000000 The application is forced to shut down. + // > - ENDSESSION_LOGOFF 0x80000000 The user is logging off. + var e = new ShutdownRequestedEventArgs() + { + IsOSShutdown = lParam == IntPtr.Zero, + }; ShutdownRequested(this, e); - if(e.Cancel) + if (e.Cancel) { return IntPtr.Zero; } @@ -207,6 +218,8 @@ namespace Avalonia.Win32 { throw new Win32Exception(); } + + TrayIconImpl.ChangeWindowMessageFilter(_hwnd); } public ITrayIconImpl CreateTrayIcon() diff --git a/src/iOS/Avalonia.iOS/AvaloniaView.cs b/src/iOS/Avalonia.iOS/AvaloniaView.cs index eaad60a860..491eacf489 100644 --- a/src/iOS/Avalonia.iOS/AvaloniaView.cs +++ b/src/iOS/Avalonia.iOS/AvaloniaView.cs @@ -76,6 +76,17 @@ namespace Avalonia.iOS { #if !TVOS MultipleTouchEnabled = true; + + if (OperatingSystem.IsIOSVersionAtLeast(13, 4) || OperatingSystem.IsMacCatalyst()) + { + var scrollGestureRecognizer = new UIPanGestureRecognizer(_input.HandleScrollWheel) + { + // Only respond to scroll events, not touches + MaximumNumberOfTouches = 0, + AllowedScrollTypesMask = UIScrollTypeMask.Discrete | UIScrollTypeMask.Continuous + }; + AddGestureRecognizer(scrollGestureRecognizer); + } #endif } } diff --git a/src/iOS/Avalonia.iOS/InputHandler.cs b/src/iOS/Avalonia.iOS/InputHandler.cs index 2a28950219..552f003a38 100644 --- a/src/iOS/Avalonia.iOS/InputHandler.cs +++ b/src/iOS/Avalonia.iOS/InputHandler.cs @@ -6,6 +6,9 @@ using Avalonia.Input.Raw; using Avalonia.Platform; using Foundation; using UIKit; +#if !TVOS +using CoreAnimation; +#endif namespace Avalonia.iOS; @@ -20,6 +23,14 @@ internal sealed class InputHandler private readonly PenDevice _penDevice = new(releasePointerOnPenUp: true); private static long _nextTouchPointId = 1; private readonly Dictionary _knownTouches = new(); + private Point? _cachedScrollLocation; + + #if !TVOS + private CADisplayLink? _momentumDisplayLink; + private double _momentumVelocityX; + private double _momentumVelocityY; + private const double DecelerationRate = 0.95; + #endif public InputHandler(AvaloniaView view, ITopLevelImpl tl) { @@ -249,6 +260,127 @@ internal sealed class InputHandler return modifier; } + public void HandleScrollWheel(UIPanGestureRecognizer recognizer) + { + switch (recognizer.State) + { + case UIGestureRecognizerState.Began: + // We've started scrolling, stop any previous inertia scrolling + // and cache the current scroll location. + StopMomentumScrolling(); + _cachedScrollLocation = recognizer.LocationInView(_view).ToAvalonia(); + return; + case UIGestureRecognizerState.Changed: + // When you are actively scrolling, we send the scroll events + SendActiveScrollEvent(recognizer); + return; + case UIGestureRecognizerState.Ended: + // When you stop scrolling, we start inertia scrolling + // UpdateInertiaScrolling will check when the inertia stops + // and will call StopMomentumScrolling + StartInertiaScrolling(recognizer); + return; + case UIGestureRecognizerState.Cancelled: + case UIGestureRecognizerState.Failed: + // If the gesture is cancelled or failed, stop. + StopMomentumScrolling(); + return; + default: + return; + } + } + + private void SendActiveScrollEvent(UIPanGestureRecognizer recognizer) + { +#if !TVOS + // iOS 13.4+ and Catalyst support scroll wheel events + if (!OperatingSystem.IsIOSVersionAtLeast(13, 4) && !OperatingSystem.IsMacCatalyst()) + return; + + var velocity = recognizer.VelocityInView(_view); + + // Use much more sensitive scaling for active scrolling to match AppKit. + // macOS uses small deltas, so we need much larger divisors + var scaleFactor = 3000.0; + + var deltaX = velocity.X / scaleFactor; + var deltaY = velocity.Y / scaleFactor; + + _tl.Input?.Invoke(new RawMouseWheelEventArgs( + _mouseDevice, + (ulong)(DateTimeOffset.UtcNow.ToUnixTimeMilliseconds()), + Root, + _cachedScrollLocation ?? new Point(0, 0), + new Vector(deltaX, deltaY), + RawInputModifiers.None + )); +#endif + } + + private void StartInertiaScrolling(UIPanGestureRecognizer recognizer) + { +#if !TVOS + // iOS 13.4+ and Catalyst support scroll wheel events + if (!OperatingSystem.IsIOSVersionAtLeast(13, 4) && !OperatingSystem.IsMacCatalyst()) + return; + + var velocity = recognizer.VelocityInView(_view); + + var scaleFactor = 800.0; + _momentumVelocityX = velocity.X / scaleFactor; + _momentumVelocityY = velocity.Y / scaleFactor; + _momentumDisplayLink = CADisplayLink.Create(UpdateInertiaScrolling); + _momentumDisplayLink.AddToRunLoop(NSRunLoop.Main, NSRunLoopMode.Common); +#endif + } + + private void StopMomentumScrolling() + { +#if !TVOS + if (_momentumDisplayLink != null) + { + // Invalidate removes it from all run loops + // https://developer.apple.com/documentation/quartzcore/cadisplaylink + _momentumDisplayLink.Invalidate(); + _momentumDisplayLink = null; + } + + _momentumVelocityX = 0; + _momentumVelocityY = 0; + _cachedScrollLocation = null; +#endif + } + + private void UpdateInertiaScrolling() + { +#if !TVOS + _momentumVelocityX *= DecelerationRate; + _momentumVelocityY *= DecelerationRate; + + var currentMagnitude = Math.Sqrt(_momentumVelocityX * _momentumVelocityX + _momentumVelocityY * _momentumVelocityY); + + if (currentMagnitude < 0.0001 || _cachedScrollLocation is null) + { + StopMomentumScrolling(); + return; + } + + // UIPanGestureRecognizer will continue to upload the location of the pointer, + // to where it would be if it was moving with the current velocity, + // even though the pointer on screen is not moving. + // We can cache the location when we start scrolling and keep it static + // until the inertia stops. + _tl.Input?.Invoke(new RawMouseWheelEventArgs( + _mouseDevice, + (ulong)(DateTimeOffset.UtcNow.ToUnixTimeMilliseconds()), + Root, + _cachedScrollLocation.Value, + new Vector(_momentumVelocityX, _momentumVelocityY), + RawInputModifiers.None + )); +#endif + } + #pragma warning disable CA1416 private static Dictionary s_keys = new() { diff --git a/src/iOS/Avalonia.iOS/InsetsManager.cs b/src/iOS/Avalonia.iOS/InsetsManager.cs index 105e208ee9..170a669841 100644 --- a/src/iOS/Avalonia.iOS/InsetsManager.cs +++ b/src/iOS/Avalonia.iOS/InsetsManager.cs @@ -35,7 +35,7 @@ internal class InsetsManager : InsetsManagerBase } public event EventHandler? DisplayEdgeToEdgeChanged; - public override bool DisplayEdgeToEdge + public override bool DisplayEdgeToEdgePreference { get => _displayEdgeToEdge; set diff --git a/src/iOS/Avalonia.iOS/Metal/MetalPlatformGraphics.cs b/src/iOS/Avalonia.iOS/Metal/MetalPlatformGraphics.cs index fb5ffc862c..23e5b08b03 100644 --- a/src/iOS/Avalonia.iOS/Metal/MetalPlatformGraphics.cs +++ b/src/iOS/Avalonia.iOS/Metal/MetalPlatformGraphics.cs @@ -2,7 +2,6 @@ using System; using System.Runtime.Versioning; using Avalonia.Platform; using Metal; -using SkiaSharp; namespace Avalonia.iOS.Metal; @@ -33,17 +32,6 @@ internal class MetalPlatformGraphics : IPlatformGraphics return null; } -#if !TVOS - using var queue = device.CreateCommandQueue(); - using var context = GRContext.CreateMetal(new GRMtlBackendContext { Device = device, Queue = queue }); - if (context is null) - { - // Can be null on macCatalyst because of older Skia bug. - // Fixed in SkiaSharp 3.0 - return null; - } -#endif - return new MetalPlatformGraphics(device); } } diff --git a/src/iOS/Avalonia.iOS/Storage/IOSStorageProvider.cs b/src/iOS/Avalonia.iOS/Storage/IOSStorageProvider.cs index 4c1bf97c6f..79d88c13b0 100644 --- a/src/iOS/Avalonia.iOS/Storage/IOSStorageProvider.cs +++ b/src/iOS/Avalonia.iOS/Storage/IOSStorageProvider.cs @@ -26,7 +26,7 @@ internal class IOSStorageProvider : IStorageProvider public bool CanOpen => true; - public bool CanSave => false; + public bool CanSave => true; public bool CanPickFolder => true; @@ -161,10 +161,72 @@ internal class IOSStorageProvider : IStorageProvider return Task.FromResult(new IOSStorageFolder(uri, wellKnownFolder)); } - public Task SaveFilePickerAsync(FilePickerSaveOptions options) + public async Task SaveFilePickerAsync(FilePickerSaveOptions options) { - return Task.FromException( - new PlatformNotSupportedException("Save file picker is not supported by iOS")); + /* + This requires a bit of dialog here... + To save a file, we need to present the user with a document picker + This requires a temp file to be created and used to "export" the file to. + When the user picks the file location and name, UIDocumentPickerViewController + will give back the URI to the real file location, which we can then use + to give back as an IStorageFile. + https://developer.apple.com/documentation/uikit/uidocumentpickerviewcontroller + Yes, it is weird, but without the temp file it will explode. + */ + + // Create a temporary file to use with the document picker + var tempFileName = StorageProviderHelpers.NameWithExtension( + options.SuggestedFileName ?? "document", + options.DefaultExtension, + options.FileTypeChoices?.FirstOrDefault()); + + var tempDir = NSFileManager.DefaultManager.GetTemporaryDirectory().Append(Guid.NewGuid().ToString(), true); + if (tempDir == null) + { + throw new InvalidOperationException("Failed to get temporary directory for save file picker"); + } + + var isDirectoryCreated = NSFileManager.DefaultManager.CreateDirectory(tempDir, true, null, out var error); + if (!isDirectoryCreated) + { + throw new InvalidOperationException("Failed to create temporary directory for save file picker"); + } + + var tempFileUrl = tempDir.Append(tempFileName, false); + + // Create an empty file at the temp location + NSData.FromBytes(0, 0).Save(tempFileUrl, false); + + UIDocumentPickerViewController documentPicker; + if (OperatingSystem.IsIOSVersionAtLeast(14)) + { + documentPicker = new UIDocumentPickerViewController(new[] { tempFileUrl }, asCopy: true); + } + else + { +#pragma warning disable CA1422 + documentPicker = new UIDocumentPickerViewController(tempFileUrl, UIDocumentPickerMode.ExportToService); +#pragma warning restore CA1422 + } + + using (documentPicker) + { + if (OperatingSystem.IsIOSVersionAtLeast(13)) + { + documentPicker.DirectoryUrl = GetUrlFromFolder(options.SuggestedStartLocation); + } + + documentPicker.Title = options.Title; + + var tcs = new TaskCompletionSource(); + documentPicker.Delegate = new PickerDelegate(urls => tcs.TrySetResult(urls)); + var urls = await ShowPicker(documentPicker, tcs); + + // Clean up the temporary directory + NSFileManager.DefaultManager.Remove(tempDir, out _); + + return urls.FirstOrDefault() is { } url ? new IOSStorageFile(url) : null; + } } public async Task> OpenFolderPickerAsync(FolderPickerOpenOptions options) diff --git a/src/tools/Avalonia.Generators/NameGenerator/Options.cs b/src/tools/Avalonia.Generators/NameGenerator/Options.cs index abdaaab72b..76663a5681 100644 --- a/src/tools/Avalonia.Generators/NameGenerator/Options.cs +++ b/src/tools/Avalonia.Generators/NameGenerator/Options.cs @@ -1,13 +1,5 @@ namespace Avalonia.Generators.NameGenerator; -internal enum Options -{ - Public = 0, - Private = 1, - Internal = 2, - Protected = 3, -} - internal enum Behavior { OnlyProperties = 0, diff --git a/src/tools/DevGenerators/CompositionGenerator/Generator.Utils.cs b/src/tools/DevGenerators/CompositionGenerator/Generator.Utils.cs index e2c5d682c1..5b66eb3e25 100644 --- a/src/tools/DevGenerators/CompositionGenerator/Generator.Utils.cs +++ b/src/tools/DevGenerators/CompositionGenerator/Generator.Utils.cs @@ -8,13 +8,6 @@ namespace Avalonia.SourceGenerator.CompositionGenerator { public partial class Generator { - static void CleanDirectory(string path) - { - Directory.CreateDirectory(path); - Directory.Delete(path, true); - Directory.CreateDirectory(path); - } - CompilationUnitSyntax Unit() => CompilationUnit().WithUsings(List(new[] { @@ -40,16 +33,6 @@ namespace Avalonia.SourceGenerator.CompositionGenerator static SyntaxToken Semicolon() => Token(SyntaxKind.SemicolonToken); - - static FieldDeclarationSyntax DeclareConstant(string type, string name, LiteralExpressionSyntax value) - => FieldDeclaration( - VariableDeclaration(ParseTypeName(type), - SingletonSeparatedList( - VariableDeclarator(name).WithInitializer(EqualsValueClause(value)) - )) - ).WithSemicolonToken(Semicolon()) - .WithModifiers(TokenList(Token(SyntaxKind.PublicKeyword), Token(SyntaxKind.ConstKeyword))); - static FieldDeclarationSyntax DeclareField(string type, string name, params SyntaxKind[] modifiers) => DeclareField(type, name, null, modifiers); diff --git a/src/tools/DevGenerators/CompositionGenerator/Generator.cs b/src/tools/DevGenerators/CompositionGenerator/Generator.cs index df2ea423de..2f27a752e2 100644 --- a/src/tools/DevGenerators/CompositionGenerator/Generator.cs +++ b/src/tools/DevGenerators/CompositionGenerator/Generator.cs @@ -530,19 +530,6 @@ return; return body.AddStatements(ParseStatement(code)); } - static ClassDeclarationSyntax WithGetPropertyForAnimation(ClassDeclarationSyntax cl, BlockSyntax body) - { - if (body.Statements.Count == 0) - return cl; - body = body.AddStatements( - ParseStatement("return base.GetPropertyForAnimation(name);")); - var method = ((MethodDeclarationSyntax) ParseMemberDeclaration( - $"public override Avalonia.Rendering.Composition.Expressions.ExpressionVariant GetPropertyForAnimation(string name){{}}")!) - .WithBody(body); - - return cl.AddMembers(method); - } - static ClassDeclarationSyntax WithGetCompositionProperty(ClassDeclarationSyntax cl, BlockSyntax body) { if (body.Statements.Count == 0) diff --git a/tests/Avalonia.Base.UnitTests/Animation/TransitionsTests.cs b/tests/Avalonia.Base.UnitTests/Animation/TransitionsTests.cs index 2737c2cebf..caaa29d05f 100644 --- a/tests/Avalonia.Base.UnitTests/Animation/TransitionsTests.cs +++ b/tests/Avalonia.Base.UnitTests/Animation/TransitionsTests.cs @@ -84,7 +84,8 @@ namespace Avalonia.Base.UnitTests.Animation var clock = new TestClock(); var i = -1; - + var completed = false; + new TransitionInstance(clock, TimeSpan.FromMilliseconds(30), TimeSpan.FromMilliseconds(70)).Subscribe( nextValue => { @@ -124,12 +125,50 @@ namespace Avalonia.Base.UnitTests.Animation Assert.Equal(1d, nextValue); break; } - }); + }, () => completed = true); for (var z = 0; z <= 10; z++) { clock.Pulse(TimeSpan.FromMilliseconds(10)); } + + Assert.True(completed); + } + + [Fact] + public void TransitionInstance_With_Delay_But_Zero_Duration_Is_Completed_After_Delay() + { + var clock = new TestClock(); + + var i = -1; + var completed = false; + + new TransitionInstance(clock, TimeSpan.FromMilliseconds(30), TimeSpan.Zero).Subscribe( + nextValue => + { + switch (i++) + { + case 0: + Assert.Equal(0, nextValue); + break; + case 1: + Assert.Equal(0, nextValue); + break; + case 2: + Assert.Equal(0, nextValue); + break; + case 3: // one iteration sooner than the test above, because the start of the transition is also the end + Assert.Equal(1, nextValue); + break; + } + }, () => completed = true); + + for (var z = 0; z <= 4; z++) + { + clock.Pulse(TimeSpan.FromMilliseconds(10)); + } + + Assert.True(completed); } } } diff --git a/tests/Avalonia.Base.UnitTests/Data/ObjectConvertersTests_Eqaul.cs b/tests/Avalonia.Base.UnitTests/Data/ObjectConvertersTests_Equal.cs similarity index 100% rename from tests/Avalonia.Base.UnitTests/Data/ObjectConvertersTests_Eqaul.cs rename to tests/Avalonia.Base.UnitTests/Data/ObjectConvertersTests_Equal.cs diff --git a/tests/Avalonia.Base.UnitTests/Data/ObjectConvertersTests_NotEqaul.cs b/tests/Avalonia.Base.UnitTests/Data/ObjectConvertersTests_NotEqual.cs similarity index 100% rename from tests/Avalonia.Base.UnitTests/Data/ObjectConvertersTests_NotEqaul.cs rename to tests/Avalonia.Base.UnitTests/Data/ObjectConvertersTests_NotEqual.cs diff --git a/tests/Avalonia.Base.UnitTests/DispatcherTests.cs b/tests/Avalonia.Base.UnitTests/DispatcherTests.cs index a667057708..1884a1ab65 100644 --- a/tests/Avalonia.Base.UnitTests/DispatcherTests.cs +++ b/tests/Avalonia.Base.UnitTests/DispatcherTests.cs @@ -505,4 +505,104 @@ public partial class DispatcherTests t.GetAwaiter().GetResult(); } } + + + [Fact] + public async Task DispatcherResumeContinuesOnUIThread() + { + using var services = new DispatcherServices(new SimpleControlledDispatcherImpl()); + + var tokenSource = new CancellationTokenSource(); + var workload = Dispatcher.UIThread.InvokeAsync( + async () => + { + Assert.True(Dispatcher.UIThread.CheckAccess()); + + await Task.Delay(1).ConfigureAwait(false); + Assert.False(Dispatcher.UIThread.CheckAccess()); + + await Dispatcher.UIThread.Resume(); + Assert.True(Dispatcher.UIThread.CheckAccess()); + + tokenSource.Cancel(); + }); + + Dispatcher.UIThread.MainLoop(tokenSource.Token); + } + + [Fact] + public async Task DispatcherYieldContinuesOnUIThread() + { + using var services = new DispatcherServices(new SimpleControlledDispatcherImpl()); + + var tokenSource = new CancellationTokenSource(); + var workload = Dispatcher.UIThread.InvokeAsync( + async () => + { + Assert.True(Dispatcher.UIThread.CheckAccess()); + + await Dispatcher.Yield(); + Assert.True(Dispatcher.UIThread.CheckAccess()); + + tokenSource.Cancel(); + }); + + Dispatcher.UIThread.MainLoop(tokenSource.Token); + } + + [Fact] + public async Task DispatcherYieldThrowsOnNonUIThread() + { + using var services = new DispatcherServices(new SimpleControlledDispatcherImpl()); + + var tokenSource = new CancellationTokenSource(); + var workload = Dispatcher.UIThread.InvokeAsync( + async () => + { + Assert.True(Dispatcher.UIThread.CheckAccess()); + + await Task.Delay(1).ConfigureAwait(false); + Assert.False(Dispatcher.UIThread.CheckAccess()); + await Assert.ThrowsAsync(async () => await Dispatcher.Yield()); + + tokenSource.Cancel(); + }); + + Dispatcher.UIThread.MainLoop(tokenSource.Token); + } + + [Fact] + public async Task AwaitWithPriorityRunsOnUIThread() + { + static async Task Workload() + { + await Task.Delay(1).ConfigureAwait(false); + Assert.False(Dispatcher.UIThread.CheckAccess()); + + return Thread.CurrentThread.ManagedThreadId; + } + + using var services = new DispatcherServices(new SimpleControlledDispatcherImpl()); + + var tokenSource = new CancellationTokenSource(); + var workload = Dispatcher.UIThread.InvokeAsync( + async () => + { + Assert.True(Dispatcher.UIThread.CheckAccess()); + Task taskWithoutResult = Workload(); + + await Dispatcher.UIThread.AwaitWithPriority(taskWithoutResult, DispatcherPriority.Default); + + Assert.True(Dispatcher.UIThread.CheckAccess()); + Task taskWithResult = Workload(); + + await Dispatcher.UIThread.AwaitWithPriority(taskWithResult, DispatcherPriority.Default); + + Assert.True(Dispatcher.UIThread.CheckAccess()); + + tokenSource.Cancel(); + }); + + Dispatcher.UIThread.MainLoop(tokenSource.Token); + } } diff --git a/tests/Avalonia.Base.UnitTests/Input/GesturesTests.cs b/tests/Avalonia.Base.UnitTests/Input/GesturesTests.cs index a2afdd0af2..e0a35b9ab2 100644 --- a/tests/Avalonia.Base.UnitTests/Input/GesturesTests.cs +++ b/tests/Avalonia.Base.UnitTests/Input/GesturesTests.cs @@ -88,6 +88,40 @@ namespace Avalonia.Base.UnitTests.Input Assert.False(raised); } + [Fact] + public void Tapped_Should_Be_Raised_From_Captured_Control() + { + Border inner = new Border() + { + Focusable = true, + Name = "Inner" + }; + Border border = new Border() + { + Focusable = true, + Child = inner, + Name = "Parent" + }; + var root = new TestRoot + { + Child = border + }; + var raised = false; + + border.PointerPressed += (s, e) => + { + e.Pointer.Capture(inner); + }; + _mouse.Click(border, MouseButton.Left); + + root.AddHandler(Gestures.TappedEvent, (_, _) => raised = true); + + _mouse.Click(border, MouseButton.Left); + + + Assert.True(raised); + } + [Fact] public void RightTapped_Should_Be_Raised_For_Right_Button() { diff --git a/tests/Avalonia.Base.UnitTests/Input/KeyboardDeviceTests.cs b/tests/Avalonia.Base.UnitTests/Input/KeyboardDeviceTests.cs index c486a66da0..5d86ce13ad 100644 --- a/tests/Avalonia.Base.UnitTests/Input/KeyboardDeviceTests.cs +++ b/tests/Avalonia.Base.UnitTests/Input/KeyboardDeviceTests.cs @@ -159,5 +159,54 @@ namespace Avalonia.Base.UnitTests.Input Assert.Equal(1, propertyChangedRaised); } + + [Fact] + public void Cancelled_Focus_Change_Should_Not_Send_Got_Focus_Event() + { + var target = new KeyboardDevice(); + var focused = new Control(); + var root = new TestRoot(); + bool focusCancelled = false; + + focused.GettingFocus += (s, e) => + { + focusCancelled = e.TryCancel(); + }; + + focused.GotFocus += (s, e) => + { + focusCancelled = false; + }; + + target.SetFocusedElement( + focused, + NavigationMethod.Unspecified, + KeyModifiers.None); + + Assert.True(focusCancelled); + } + + [Fact] + public void Redirected_Focus_Should_Change_Focused_Element() + { + var target = new KeyboardDevice(); + var first = new Control(); + var second = new Control(); + var stack = new StackPanel(); + stack.Children.AddRange(new[] { first, second }); + var root = new TestRoot(stack); + + first.GettingFocus += (s, e) => + { + e.TrySetNewFocusedElement(second); + }; + + target.SetFocusedElement( + first, + NavigationMethod.Unspecified, + KeyModifiers.None); + + Assert.True(second.IsFocused); + } } } diff --git a/tests/Avalonia.Base.UnitTests/Layout/LayoutableTests.cs b/tests/Avalonia.Base.UnitTests/Layout/LayoutableTests.cs index b8cba8c169..5784c46964 100644 --- a/tests/Avalonia.Base.UnitTests/Layout/LayoutableTests.cs +++ b/tests/Avalonia.Base.UnitTests/Layout/LayoutableTests.cs @@ -435,6 +435,35 @@ namespace Avalonia.Base.UnitTests.Layout }); } + [Fact] + public void Constraint_And_Negative_Margin() + { + using var app = UnitTestApplication.Start(TestServices.MockPlatformRenderInterface); + + var textBlock = new TextBlock + { + Margin = new Thickness(-10), + Text = "Lorem ipsum dolor sit amet", + }; + + var border = new Border + { + MaxWidth = 100, + Child = textBlock, + }; + + border.Measure(new Size(double.PositiveInfinity, double.PositiveInfinity)); + border.Arrange(new Rect(default, border.DesiredSize)); + + Assert.Multiple(() => + { + Assert.Equal(new Size(100, 0), border.DesiredSize); + Assert.Equal(new Rect(0, 0, 100, 0), border.Bounds); + Assert.Equal(new Size(100, 0), textBlock.DesiredSize); + Assert.Equal(new Rect(-10, -10, 120, 20), textBlock.Bounds); + }); + } + private class TestLayoutable : Layoutable { public Size ArrangeSize { get; private set; } diff --git a/tests/Avalonia.Controls.UnitTests/GridTests.cs b/tests/Avalonia.Controls.UnitTests/GridTests.cs index c1bbc5acc3..7205db7950 100644 --- a/tests/Avalonia.Controls.UnitTests/GridTests.cs +++ b/tests/Avalonia.Controls.UnitTests/GridTests.cs @@ -1900,6 +1900,36 @@ namespace Avalonia.Controls.UnitTests Assert.Equal(grid1.Children[4].Bounds.Width, grid2.Children[0].Bounds.Width); } + + [Fact] + public void Grid_With_ColumnSpacing_And_ColumnDefinitions_Unset() + { + var target = new Grid + { + Height = 300, + Width = 100, + ColumnSpacing = 10, + RowDefinitions = RowDefinitions.Parse("Auto,*"),//Set RowDefinitions to avoid + Children = + { + new Border + { + [Grid.RowProperty] = 0, + Height = 80, + Margin = new Thickness(10), + }, + new Border + { + [Grid.RowProperty] = 1, + Margin = new Thickness(20), + }, + }, + }; + target.Measure(new Size(100, 300)); + target.Arrange(new Rect(target.DesiredSize)); + Assert.Equal(new Rect(10, 10, 80, 80), target.Children[0].Bounds); + Assert.Equal(new Rect(20, 120, 60, 160),target.Children[1].Bounds); + } private class TestControl : Control { public Size MeasureSize { get; set; } diff --git a/tests/Avalonia.Controls.UnitTests/Primitives/PopupTests.cs b/tests/Avalonia.Controls.UnitTests/Primitives/PopupTests.cs index 139c4656a1..1a9ce7c655 100644 --- a/tests/Avalonia.Controls.UnitTests/Primitives/PopupTests.cs +++ b/tests/Avalonia.Controls.UnitTests/Primitives/PopupTests.cs @@ -669,6 +669,51 @@ namespace Avalonia.Controls.UnitTests.Primitives } } + [Fact] + public void Popup_Should_Clear_Keyboard_Focus_From_Children_When_Closed() + { + using (CreateServicesWithFocus()) + { + var winButton = new Button(); + var window = PreparedWindow(new Panel { Children = { winButton }}); + + var border1 = new Border(); + var border2 = new Border(); + var button = new Button(); + border1.Child = border2; + border2.Child = button; + var popup = new Popup + { + PlacementTarget = window, + Child = new StackPanel + { + Children = + { + border1 + } + } + }; + + ((ISetLogicalParent)popup).SetParent(popup.PlacementTarget); + window.Show(); + winButton.Focus(); + popup.Open(); + + button.Focus(); + + var inputRoot = Assert.IsAssignableFrom(popup.Host); + + var focusManager = inputRoot.FocusManager!; + Assert.Same(button, focusManager.GetFocusedElement()); + + border1.Child = null; + + winButton.Focus(); + + Assert.False(border2.IsKeyboardFocusWithin); + } + } + [Fact] public void Closing_Popup_Sets_Focus_On_PlacementTarget() { diff --git a/tests/Avalonia.Controls.UnitTests/TextBlockTests.cs b/tests/Avalonia.Controls.UnitTests/TextBlockTests.cs index fd854ed2b3..ee075e1cda 100644 --- a/tests/Avalonia.Controls.UnitTests/TextBlockTests.cs +++ b/tests/Avalonia.Controls.UnitTests/TextBlockTests.cs @@ -458,6 +458,80 @@ namespace Avalonia.Controls.UnitTests Assert.True(target.DesiredSize.Height > 0); } + [Fact] + public void TextBlock_With_UseLayoutRounding_True_Should_Round_DesiredSize() + { + using var app = UnitTestApplication.Start(TestServices.MockPlatformRenderInterface); + + var target = new TextBlock { Text = "1980" }; + + target.Measure(new Size(double.PositiveInfinity, double.PositiveInfinity)); + + Assert.Equal(target.DesiredSize, new Size(40, 10)); + } + + [Fact] + public void TextBlock_With_UseLayoutRounding_True_Should_Round_Padding_And_DesiredSize() + { + using var app = UnitTestApplication.Start(TestServices.MockPlatformRenderInterface); + + var target = new TextBlock { Text = "1980", Padding = new(2.25) }; + + target.Measure(new Size(double.PositiveInfinity, double.PositiveInfinity)); + + Assert.Equal(target.DesiredSize, new Size(44, 14)); + } + + [Fact] + public void TextBlock_With_UseLayoutRounding_False_Should_Not_Round_DesiredSize() + { + using var app = UnitTestApplication.Start(TestServices.MockPlatformRenderInterface); + + var target = new TextBlock { Text = "1980", UseLayoutRounding = false }; + + target.Measure(new Size(double.PositiveInfinity, double.PositiveInfinity)); + + Assert.Equal(target.DesiredSize, new Size(40, 9.6)); + } + + [Fact] + public void TextBlock_With_UseLayoutRounding_False_Should_Not_Round_Bounds() + { + using var app = UnitTestApplication.Start(TestServices.MockPlatformRenderInterface); + + var target = new TextBlock { Text = "1980", UseLayoutRounding = false }; + + target.Measure(new Size(double.PositiveInfinity, double.PositiveInfinity)); + target.Arrange(new Rect(default, target.DesiredSize)); + + Assert.Equal(target.Bounds, new Rect(0, 0, 40, 9.6)); + } + + [Fact] + public void TextBlock_With_UseLayoutRounding_False_Should_Not_Round_Padding_In_MeasureOverride() + { + using var app = UnitTestApplication.Start(TestServices.MockPlatformRenderInterface); + + var target = new TextBlock { Text = "1980", UseLayoutRounding = false, Padding = new(2.25) }; + + target.Measure(new Size(double.PositiveInfinity, double.PositiveInfinity)); + + Assert.Equal(target.DesiredSize, new Size(44.5, 14.1)); + } + + [Fact] + public void TextBlock_With_UseLayoutRounding_False_Should_Not_Round_Padding_In_ArrangeOverride() + { + using var app = UnitTestApplication.Start(TestServices.MockPlatformRenderInterface); + + var target = new TextBlock { Text = "1980", UseLayoutRounding = false, Padding = new(2.25) }; + + target.Measure(new Size(double.PositiveInfinity, double.PositiveInfinity)); + target.Arrange(new Rect(default, target.DesiredSize)); + + Assert.Equal(target.Bounds, new Rect(0, 0, 44.5, 14.1)); + } + private class TestTextBlock : TextBlock { public Size Constraint => _constraint; diff --git a/tests/Avalonia.Controls.UnitTests/Utils/BindingEvaluatorTests.cs b/tests/Avalonia.Controls.UnitTests/Utils/BindingEvaluatorTests.cs new file mode 100644 index 0000000000..41fb225353 --- /dev/null +++ b/tests/Avalonia.Controls.UnitTests/Utils/BindingEvaluatorTests.cs @@ -0,0 +1,21 @@ +#nullable enable + +using Avalonia.Controls.Utils; +using Avalonia.UnitTests; +using Xunit; + +namespace Avalonia.Controls.UnitTests.Utils; + +public class BindingEvaluatorTests : ScopedTestBase +{ + [Fact] + public void ClearDataContext_Sets_DataContext_To_Null() + { + var evaluator = new BindingEvaluator(); + evaluator.Evaluate("foo"); + Assert.Equal("foo", evaluator.DataContext); + + evaluator.ClearDataContext(); + Assert.Null(evaluator.DataContext); + } +} diff --git a/tests/Avalonia.Controls.UnitTests/VirtualizingStackPanelTests.cs b/tests/Avalonia.Controls.UnitTests/VirtualizingStackPanelTests.cs index 0b33239687..6c6252d836 100644 --- a/tests/Avalonia.Controls.UnitTests/VirtualizingStackPanelTests.cs +++ b/tests/Avalonia.Controls.UnitTests/VirtualizingStackPanelTests.cs @@ -3,6 +3,7 @@ using System.Collections; using System.Collections.Generic; using System.Collections.ObjectModel; using System.Collections.Specialized; +using System.Diagnostics; using System.Linq; using Avalonia.Collections; using Avalonia.Controls.Presenters; @@ -24,137 +25,167 @@ namespace Avalonia.Controls.UnitTests public class VirtualizingStackPanelTests : ScopedTestBase { private static FuncDataTemplate CanvasWithHeightTemplate = new((_, _) => - new Canvas + new CanvasCountingMeasureArrangeCalls { Width = 100, [!Layoutable.HeightProperty] = new Binding("Height"), }); private static FuncDataTemplate CanvasWithWidthTemplate = new((_, _) => - new Canvas + new CanvasCountingMeasureArrangeCalls { Height = 100, [!Layoutable.WidthProperty] = new Binding("Width"), }); - [Fact] - public void Creates_Initial_Items() + [Theory] + [InlineData(0d , 10)] + [InlineData(0.5d, 20)] + public void Creates_Initial_Items(double bufferFactor, int expectedCount) { using var app = App(); - var (target, scroll, itemsControl) = CreateTarget(); + var (target, scroll, itemsControl) = CreateTarget(bufferFactor:bufferFactor); Assert.Equal(1000, scroll.Extent.Height); - AssertRealizedItems(target, itemsControl, 0, 10); + AssertRealizedItems(target, itemsControl, 0, expectedCount); } - [Fact] - public void Initializes_Initial_Control_Items() + [Theory] + [InlineData(0d, 10)] + [InlineData(0.5d, 20)] // Buffer factor of 0.5. Since at start there is no room, the 10 additional items are just appended + public void Initializes_Initial_Control_Items(double bufferFactor, int expectedCount) { using var app = App(); var items = Enumerable.Range(0, 100).Select(x => new Button { Width = 25, Height = 10 }); - var (target, scroll, itemsControl) = CreateTarget(items: items, itemTemplate: null); + var (target, scroll, itemsControl) = CreateTarget(items: items, itemTemplate: null, bufferFactor:bufferFactor); Assert.Equal(1000, scroll.Extent.Height); - AssertRealizedControlItems