diff --git a/Avalonia.sln b/Avalonia.sln
index c000f56d09..461de8530b 100644
--- a/Avalonia.sln
+++ b/Avalonia.sln
@@ -41,6 +41,7 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Shared", "Shared", "{A689DE
src\Shared\IsExternalInit.cs = src\Shared\IsExternalInit.cs
src\Shared\ModuleInitializer.cs = src\Shared\ModuleInitializer.cs
src\Shared\SourceGeneratorAttributes.cs = src\Shared\SourceGeneratorAttributes.cs
+ src\Avalonia.Base\Compatibility\StringCompatibilityExtensions.cs = src\Avalonia.Base\Compatibility\StringCompatibilityExtensions.cs
EndProjectSection
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Avalonia.ReactiveUI", "src\Avalonia.ReactiveUI\Avalonia.ReactiveUI.csproj", "{6417B24E-49C2-4985-8DB2-3AB9D898EC91}"
diff --git a/Directory.Build.targets b/Directory.Build.targets
new file mode 100644
index 0000000000..73954c7f4d
--- /dev/null
+++ b/Directory.Build.targets
@@ -0,0 +1,5 @@
+
+
+ $(DefineConstants);NET7SDK
+
+
diff --git a/native/Avalonia.Native/src/OSX/AvnWindow.mm b/native/Avalonia.Native/src/OSX/AvnWindow.mm
index ebd9f39d30..4c7341f834 100644
--- a/native/Avalonia.Native/src/OSX/AvnWindow.mm
+++ b/native/Avalonia.Native/src/OSX/AvnWindow.mm
@@ -385,7 +385,7 @@
return true;
}
--(void)resignKeyWindow
+-(void)windowDidResignKey:(NSNotification *)notification
{
if(_parent)
_parent->BaseEvents->Deactivated();
@@ -393,8 +393,6 @@
[self showAppMenuOnly];
[self invalidateShadow];
-
- [super resignKeyWindow];
}
- (void)windowDidMove:(NSNotification *_Nonnull)notification
diff --git a/native/Avalonia.Native/src/OSX/WindowImpl.mm b/native/Avalonia.Native/src/OSX/WindowImpl.mm
index ddc50c26b6..2443965957 100644
--- a/native/Avalonia.Native/src/OSX/WindowImpl.mm
+++ b/native/Avalonia.Native/src/OSX/WindowImpl.mm
@@ -63,7 +63,7 @@ HRESULT WindowImpl::Show(bool activate, bool isDialog) {
START_COM_CALL;
@autoreleasepool {
- _isDialog = isDialog;
+ _isDialog = isDialog || _parent != nullptr;
WindowBaseImpl::Show(activate, isDialog);
@@ -96,6 +96,8 @@ HRESULT WindowImpl::SetParent(IAvnWindow *parent) {
auto cparent = dynamic_cast(parent);
_parent = cparent;
+
+ _isDialog = _parent != nullptr;
if(_parent != nullptr && Window != nullptr){
// If one tries to show a child window with a minimized parent window, then the parent window will be
diff --git a/native/Avalonia.Native/src/OSX/app.mm b/native/Avalonia.Native/src/OSX/app.mm
index a15d0c9601..9cc9fc9523 100644
--- a/native/Avalonia.Native/src/OSX/app.mm
+++ b/native/Avalonia.Native/src/OSX/app.mm
@@ -95,11 +95,14 @@ ComPtr _events;
}
@end
-extern void InitializeAvnApp(IAvnApplicationEvents* events)
+extern void InitializeAvnApp(IAvnApplicationEvents* events, bool disableAppDelegate)
{
- NSApplication* app = [AvnApplication sharedApplication];
- id delegate = [[AvnAppDelegate alloc] initWithEvents:events];
- [app setDelegate:delegate];
+ if(!disableAppDelegate)
+ {
+ NSApplication* app = [AvnApplication sharedApplication];
+ id delegate = [[AvnAppDelegate alloc] initWithEvents:events];
+ [app setDelegate:delegate];
+ }
}
HRESULT AvnApplicationCommands::HideApp()
diff --git a/native/Avalonia.Native/src/OSX/common.h b/native/Avalonia.Native/src/OSX/common.h
index a90a235b9d..7ee7205776 100644
--- a/native/Avalonia.Native/src/OSX/common.h
+++ b/native/Avalonia.Native/src/OSX/common.h
@@ -32,7 +32,7 @@ extern void SetServicesMenu (IAvnMenu* menu);
extern IAvnMenu* GetAppMenu ();
extern NSMenuItem* GetAppMenuItem ();
-extern void InitializeAvnApp(IAvnApplicationEvents* events);
+extern void InitializeAvnApp(IAvnApplicationEvents* events, bool disableAppDelegate);
extern NSApplicationActivationPolicy AvnDesiredActivationPolicy;
extern NSPoint ToNSPoint (AvnPoint p);
extern NSRect ToNSRect (AvnRect r);
diff --git a/native/Avalonia.Native/src/OSX/main.mm b/native/Avalonia.Native/src/OSX/main.mm
index 6ee86b21ae..c29d4108d0 100644
--- a/native/Avalonia.Native/src/OSX/main.mm
+++ b/native/Avalonia.Native/src/OSX/main.mm
@@ -3,6 +3,8 @@
#include "common.h"
static NSString* s_appTitle = @"Avalonia";
+static int disableSetProcessName = 0;
+static bool disableAppDelegate = false;
// Copyright (c) 2011 The Chromium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
@@ -111,11 +113,17 @@ public:
@autoreleasepool
{
auto appTitle = [NSString stringWithUTF8String: utf8String];
-
- [[NSProcessInfo processInfo] setProcessName:appTitle];
-
-
- SetProcessName(appTitle);
+ if (disableSetProcessName == 0)
+ {
+ [[NSProcessInfo processInfo] setProcessName:appTitle];
+
+ SetProcessName(appTitle);
+ }
+ if (disableSetProcessName == 1)
+ {
+ auto rootMenu = [NSApp mainMenu];
+ [rootMenu setTitle:appTitle];
+ }
return S_OK;
}
@@ -133,6 +141,27 @@ public:
}
}
+ virtual HRESULT SetDisableSetProcessName(int disable) override
+ {
+ START_COM_CALL;
+
+ @autoreleasepool
+ {
+ disableSetProcessName = disable;
+ return S_OK;
+ }
+ }
+
+ virtual HRESULT SetDisableAppDelegate(int disable) override
+ {
+ START_COM_CALL;
+
+ @autoreleasepool {
+ disableAppDelegate = disable;
+ return S_OK;
+ }
+ }
+
};
/// See "Using POSIX Threads in a Cocoa Application" section here:
@@ -175,7 +204,7 @@ public:
@autoreleasepool{
[[ThreadingInitializer new] do];
}
- InitializeAvnApp(events);
+ InitializeAvnApp(events, disableAppDelegate);
return S_OK;
};
diff --git a/nukebuild/_build.csproj b/nukebuild/_build.csproj
index 8c0d824298..865d935ad7 100644
--- a/nukebuild/_build.csproj
+++ b/nukebuild/_build.csproj
@@ -13,7 +13,7 @@
-
+
diff --git a/readme.md b/readme.md
index 1009e86c29..c2be487af3 100644
--- a/readme.md
+++ b/readme.md
@@ -104,7 +104,7 @@ Support this project by becoming a sponsor. Your logo will show up here with a l
## Commercial Support
-We have a range of [support plans available](https://avaloniaui.net/support.html) for those looking to partner with the creators of Avalonia, enabling access to the best support at every step of the development process.
+We have a range of [support plans available](https://avaloniaui.net/support) for those looking to partner with the creators of Avalonia, enabling access to the best support at every step of the development process.
*Please note that donations are not considered payment for commercial support agreements. Please contact us to discuss your needs first. [team@avaloniaui.net](mailto://team@avaloniaui.net)*
## .NET Foundation
diff --git a/samples/ControlCatalog/Pages/ColorPickerPage.xaml b/samples/ControlCatalog/Pages/ColorPickerPage.xaml
index 69ceaea328..c0bb95ae92 100644
--- a/samples/ControlCatalog/Pages/ColorPickerPage.xaml
+++ b/samples/ControlCatalog/Pages/ColorPickerPage.xaml
@@ -24,18 +24,19 @@
HsvColor="hsv(120, 1, 1)"
Margin="0,50,0,0">
-
+
+ RowDefinitions="Auto,Auto,Auto,Auto,Auto,Auto,Auto,Auto,Auto">
+
-
+
+
diff --git a/samples/ControlCatalog/Pages/ColorPickerPage.xaml.cs b/samples/ControlCatalog/Pages/ColorPickerPage.xaml.cs
index 4671bbdb7c..52d63ded32 100644
--- a/samples/ControlCatalog/Pages/ColorPickerPage.xaml.cs
+++ b/samples/ControlCatalog/Pages/ColorPickerPage.xaml.cs
@@ -20,6 +20,7 @@ namespace ControlCatalog.Pages
Color = Colors.Blue,
Margin = new Thickness(0, 50, 0, 0),
HorizontalAlignment = HorizontalAlignment.Center,
+ Palette = new MaterialHalfColorPalette(),
};
Grid.SetColumn(colorPicker, 2);
Grid.SetRow(colorPicker, 1);
diff --git a/samples/ControlCatalog/Pages/ExpanderPage.xaml b/samples/ControlCatalog/Pages/ExpanderPage.xaml
index f9ae1c7a3c..8c8702c665 100644
--- a/samples/ControlCatalog/Pages/ExpanderPage.xaml
+++ b/samples/ControlCatalog/Pages/ExpanderPage.xaml
@@ -32,6 +32,23 @@
Expanded content
+
+
+
+
+
+ Expanded content
+
+
+
+
+ Expanded content
+
+
Rounded
diff --git a/samples/ControlCatalog/Pages/TextBlockPage.xaml b/samples/ControlCatalog/Pages/TextBlockPage.xaml
index 32914428ed..6bb428e2c7 100644
--- a/samples/ControlCatalog/Pages/TextBlockPage.xaml
+++ b/samples/ControlCatalog/Pages/TextBlockPage.xaml
@@ -118,7 +118,7 @@
-
+
This is a
TextBlock
with several
@@ -126,7 +126,7 @@
using a variety of styles
.
-
+
diff --git a/src/Avalonia.Base/Avalonia.Base.csproj b/src/Avalonia.Base/Avalonia.Base.csproj
index 402bc3a099..bdb5882ef2 100644
--- a/src/Avalonia.Base/Avalonia.Base.csproj
+++ b/src/Avalonia.Base/Avalonia.Base.csproj
@@ -23,6 +23,7 @@
+
@@ -46,6 +47,7 @@
+
diff --git a/src/Avalonia.Base/Collections/AvaloniaDictionary.cs b/src/Avalonia.Base/Collections/AvaloniaDictionary.cs
index 750fb263f5..35a391f2cb 100644
--- a/src/Avalonia.Base/Collections/AvaloniaDictionary.cs
+++ b/src/Avalonia.Base/Collections/AvaloniaDictionary.cs
@@ -81,7 +81,7 @@ namespace Avalonia.Collections
if (replace)
{
- PropertyChanged?.Invoke(this, new PropertyChangedEventArgs($"Item[{key}]"));
+ PropertyChanged?.Invoke(this, new PropertyChangedEventArgs($"{CommonPropertyNames.IndexerName}[{key}]"));
if (CollectionChanged != null)
{
@@ -148,7 +148,7 @@ namespace Avalonia.Collections
{
_inner.Remove(key);
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(nameof(Count)));
- PropertyChanged?.Invoke(this, new PropertyChangedEventArgs($"Item[{key}]"));
+ PropertyChanged?.Invoke(this, new PropertyChangedEventArgs($"{CommonPropertyNames.IndexerName}[{key}]"));
if (CollectionChanged != null)
{
@@ -208,7 +208,7 @@ namespace Avalonia.Collections
private void NotifyAdd(TKey key, TValue value)
{
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(nameof(Count)));
- PropertyChanged?.Invoke(this, new PropertyChangedEventArgs($"Item[{key}]"));
+ PropertyChanged?.Invoke(this, new PropertyChangedEventArgs($"{CommonPropertyNames.IndexerName}[{key}]"));
if (CollectionChanged != null)
diff --git a/src/Avalonia.Base/Controls/NameScopeEventArgs.cs b/src/Avalonia.Base/Controls/NameScopeEventArgs.cs
deleted file mode 100644
index 3e9eaa6057..0000000000
--- a/src/Avalonia.Base/Controls/NameScopeEventArgs.cs
+++ /dev/null
@@ -1,16 +0,0 @@
-using System;
-
-namespace Avalonia.Controls
-{
- public class NameScopeEventArgs : EventArgs
- {
- public NameScopeEventArgs(string name, object element)
- {
- Name = name;
- Element = element;
- }
-
- public string Name { get; }
- public object Element { get; }
- }
-}
diff --git a/src/Avalonia.Base/Input/DragEventArgs.cs b/src/Avalonia.Base/Input/DragEventArgs.cs
index 0e613c0f21..41276e2f06 100644
--- a/src/Avalonia.Base/Input/DragEventArgs.cs
+++ b/src/Avalonia.Base/Input/DragEventArgs.cs
@@ -32,7 +32,7 @@ namespace Avalonia.Input
return point;
}
- public DragEventArgs(RoutedEvent routedEvent, IDataObject data, Interactive target, Point targetLocation, KeyModifiers keyModifiers)
+ internal DragEventArgs(RoutedEvent routedEvent, IDataObject data, Interactive target, Point targetLocation, KeyModifiers keyModifiers)
: base(routedEvent)
{
Data = data;
diff --git a/src/Avalonia.Base/Input/GotFocusEventArgs.cs b/src/Avalonia.Base/Input/GotFocusEventArgs.cs
index 5cce138ee0..f3de55ebae 100644
--- a/src/Avalonia.Base/Input/GotFocusEventArgs.cs
+++ b/src/Avalonia.Base/Input/GotFocusEventArgs.cs
@@ -7,6 +7,11 @@ namespace Avalonia.Input
///
public class GotFocusEventArgs : RoutedEventArgs
{
+ internal GotFocusEventArgs()
+ {
+
+ }
+
///
/// Gets or sets a value indicating how the change in focus occurred.
///
diff --git a/src/Avalonia.Base/Input/KeyEventArgs.cs b/src/Avalonia.Base/Input/KeyEventArgs.cs
index b8291e9096..39c9766105 100644
--- a/src/Avalonia.Base/Input/KeyEventArgs.cs
+++ b/src/Avalonia.Base/Input/KeyEventArgs.cs
@@ -5,6 +5,11 @@ namespace Avalonia.Input
{
public class KeyEventArgs : RoutedEventArgs
{
+ internal KeyEventArgs()
+ {
+
+ }
+
public IKeyboardDevice? Device { get; set; }
public Key Key { get; set; }
diff --git a/src/Avalonia.Base/Input/PointerDeltaEventArgs.cs b/src/Avalonia.Base/Input/PointerDeltaEventArgs.cs
index b3085a038d..d5577d77af 100644
--- a/src/Avalonia.Base/Input/PointerDeltaEventArgs.cs
+++ b/src/Avalonia.Base/Input/PointerDeltaEventArgs.cs
@@ -7,7 +7,7 @@ namespace Avalonia.Input
{
public Vector Delta { get; set; }
- public PointerDeltaEventArgs(RoutedEvent routedEvent, IInteractive? source,
+ internal PointerDeltaEventArgs(RoutedEvent routedEvent, IInteractive? source,
IPointer pointer, IVisual rootVisual, Point rootVisualPosition, ulong timestamp,
PointerPointProperties properties, KeyModifiers modifiers, Vector delta)
: base(routedEvent, source, pointer, rootVisual, rootVisualPosition,
diff --git a/src/Avalonia.Base/Input/PointerEventArgs.cs b/src/Avalonia.Base/Input/PointerEventArgs.cs
index a0e8d4a074..f84dec42cb 100644
--- a/src/Avalonia.Base/Input/PointerEventArgs.cs
+++ b/src/Avalonia.Base/Input/PointerEventArgs.cs
@@ -13,7 +13,7 @@ namespace Avalonia.Input
private readonly PointerPointProperties _properties;
private readonly Lazy?>? _previousPoints;
- public PointerEventArgs(RoutedEvent routedEvent,
+ internal PointerEventArgs(RoutedEvent routedEvent,
IInteractive? source,
IPointer pointer,
IVisual? rootVisual, Point rootVisualPosition,
@@ -30,8 +30,8 @@ namespace Avalonia.Input
Timestamp = timestamp;
KeyModifiers = modifiers;
}
-
- public PointerEventArgs(RoutedEvent routedEvent,
+
+ internal PointerEventArgs(RoutedEvent routedEvent,
IInteractive? source,
IPointer pointer,
IVisual? rootVisual, Point rootVisualPosition,
@@ -124,7 +124,7 @@ namespace Avalonia.Input
public class PointerPressedEventArgs : PointerEventArgs
{
- public PointerPressedEventArgs(
+ internal PointerPressedEventArgs(
IInteractive source,
IPointer pointer,
IVisual rootVisual, Point rootVisualPosition,
@@ -143,7 +143,7 @@ namespace Avalonia.Input
public class PointerReleasedEventArgs : PointerEventArgs
{
- public PointerReleasedEventArgs(
+ internal PointerReleasedEventArgs(
IInteractive source, IPointer pointer,
IVisual rootVisual, Point rootVisualPosition, ulong timestamp,
PointerPointProperties properties, KeyModifiers modifiers,
@@ -164,7 +164,7 @@ namespace Avalonia.Input
{
public IPointer Pointer { get; }
- public PointerCaptureLostEventArgs(IInteractive source, IPointer pointer) : base(InputElement.PointerCaptureLostEvent)
+ internal PointerCaptureLostEventArgs(IInteractive source, IPointer pointer) : base(InputElement.PointerCaptureLostEvent)
{
Pointer = pointer;
Source = source;
diff --git a/src/Avalonia.Base/Input/PointerWheelEventArgs.cs b/src/Avalonia.Base/Input/PointerWheelEventArgs.cs
index e5701dcf23..dbc06ec934 100644
--- a/src/Avalonia.Base/Input/PointerWheelEventArgs.cs
+++ b/src/Avalonia.Base/Input/PointerWheelEventArgs.cs
@@ -7,7 +7,7 @@ namespace Avalonia.Input
{
public Vector Delta { get; set; }
- public PointerWheelEventArgs(IInteractive source, IPointer pointer, IVisual rootVisual,
+ internal PointerWheelEventArgs(IInteractive source, IPointer pointer, IVisual rootVisual,
Point rootVisualPosition, ulong timestamp,
PointerPointProperties properties, KeyModifiers modifiers, Vector delta)
: base(InputElement.PointerWheelChangedEvent, source, pointer, rootVisual, rootVisualPosition,
diff --git a/src/Avalonia.Base/Input/ScrollGestureEventArgs.cs b/src/Avalonia.Base/Input/ScrollGestureEventArgs.cs
index a682e8f0a4..fd1d0f42c3 100644
--- a/src/Avalonia.Base/Input/ScrollGestureEventArgs.cs
+++ b/src/Avalonia.Base/Input/ScrollGestureEventArgs.cs
@@ -9,8 +9,8 @@ namespace Avalonia.Input
private static int _nextId = 1;
public static int GetNextFreeId() => _nextId++;
-
- public ScrollGestureEventArgs(int id, Vector delta) : base(Gestures.ScrollGestureEvent)
+
+ internal ScrollGestureEventArgs(int id, Vector delta) : base(Gestures.ScrollGestureEvent)
{
Id = id;
Delta = delta;
@@ -21,7 +21,7 @@ namespace Avalonia.Input
{
public int Id { get; }
- public ScrollGestureEndedEventArgs(int id) : base(Gestures.ScrollGestureEndedEvent)
+ internal ScrollGestureEndedEventArgs(int id) : base(Gestures.ScrollGestureEndedEvent)
{
Id = id;
}
diff --git a/src/Avalonia.Base/Input/TappedEventArgs.cs b/src/Avalonia.Base/Input/TappedEventArgs.cs
index daaab70632..8af6164fc1 100644
--- a/src/Avalonia.Base/Input/TappedEventArgs.cs
+++ b/src/Avalonia.Base/Input/TappedEventArgs.cs
@@ -7,7 +7,7 @@ namespace Avalonia.Input
{
private readonly PointerEventArgs lastPointerEventArgs;
- public TappedEventArgs(RoutedEvent routedEvent, PointerEventArgs lastPointerEventArgs)
+ internal TappedEventArgs(RoutedEvent routedEvent, PointerEventArgs lastPointerEventArgs)
: base(routedEvent)
{
this.lastPointerEventArgs = lastPointerEventArgs;
diff --git a/src/Avalonia.Base/Input/TextInputEventArgs.cs b/src/Avalonia.Base/Input/TextInputEventArgs.cs
index cda0103749..787bf1abd3 100644
--- a/src/Avalonia.Base/Input/TextInputEventArgs.cs
+++ b/src/Avalonia.Base/Input/TextInputEventArgs.cs
@@ -4,6 +4,10 @@ namespace Avalonia.Input
{
public class TextInputEventArgs : RoutedEventArgs
{
+ internal TextInputEventArgs()
+ {
+
+ }
public IKeyboardDevice? Device { get; set; }
public string? Text { get; set; }
diff --git a/src/Avalonia.Base/Input/VectorEventArgs.cs b/src/Avalonia.Base/Input/VectorEventArgs.cs
index 000fd52f69..3e8098f904 100644
--- a/src/Avalonia.Base/Input/VectorEventArgs.cs
+++ b/src/Avalonia.Base/Input/VectorEventArgs.cs
@@ -5,6 +5,11 @@ namespace Avalonia.Input
{
public class VectorEventArgs : RoutedEventArgs
{
+ internal VectorEventArgs()
+ {
+
+ }
+
public Vector Vector { get; set; }
}
}
diff --git a/src/Avalonia.Base/Interactivity/RoutedEventArgs.cs b/src/Avalonia.Base/Interactivity/RoutedEventArgs.cs
index ccbb41b7dc..2b660e7080 100644
--- a/src/Avalonia.Base/Interactivity/RoutedEventArgs.cs
+++ b/src/Avalonia.Base/Interactivity/RoutedEventArgs.cs
@@ -2,29 +2,59 @@ using System;
namespace Avalonia.Interactivity
{
+ ///
+ /// Provides state information and data specific to a routed event.
+ ///
public class RoutedEventArgs : EventArgs
{
+ ///
+ /// Initializes a new instance of the class.
+ ///
public RoutedEventArgs()
{
}
+ ///
+ /// Initializes a new instance of the class.
+ ///
+ /// The routed event associated with these event args.
public RoutedEventArgs(RoutedEvent? routedEvent)
{
RoutedEvent = routedEvent;
}
+ ///
+ /// Initializes a new instance of the class.
+ ///
+ /// The routed event associated with these event args.
+ /// The source object that raised the routed event.
public RoutedEventArgs(RoutedEvent? routedEvent, IInteractive? source)
{
RoutedEvent = routedEvent;
Source = source;
}
+ ///
+ /// Gets or sets a value indicating whether the routed event has already been handled.
+ ///
+ ///
+ /// Once handled, a routed event should be ignored.
+ ///
public bool Handled { get; set; }
+ ///
+ /// Gets or sets the routed event associated with these event args.
+ ///
public RoutedEvent? RoutedEvent { get; set; }
+ ///
+ /// Gets or sets the routing strategy (direct, bubbling, or tunneling) of the routed event.
+ ///
public RoutingStrategies Route { get; set; }
+ ///
+ /// Gets or sets the source object that raised the routed event.
+ ///
public IInteractive? Source { get; set; }
}
}
diff --git a/src/Avalonia.Base/Layout/EffectiveViewportChangedEventArgs.cs b/src/Avalonia.Base/Layout/EffectiveViewportChangedEventArgs.cs
index 1cdc775b13..749d2ecc2b 100644
--- a/src/Avalonia.Base/Layout/EffectiveViewportChangedEventArgs.cs
+++ b/src/Avalonia.Base/Layout/EffectiveViewportChangedEventArgs.cs
@@ -7,7 +7,7 @@ namespace Avalonia.Layout
///
public class EffectiveViewportChangedEventArgs : EventArgs
{
- public EffectiveViewportChangedEventArgs(Rect effectiveViewport)
+ internal EffectiveViewportChangedEventArgs(Rect effectiveViewport)
{
EffectiveViewport = effectiveViewport;
}
diff --git a/src/Avalonia.Base/Media/FontSimulations.cs b/src/Avalonia.Base/Media/FontSimulations.cs
new file mode 100644
index 0000000000..2faf53f1d8
--- /dev/null
+++ b/src/Avalonia.Base/Media/FontSimulations.cs
@@ -0,0 +1,27 @@
+using System;
+
+namespace Avalonia.Media
+{
+ ///
+ /// Specifies algorithmic style simulations to be applied to the typeface.
+ /// Bold and oblique simulations can be combined via bitwise OR operation.
+ ///
+ [Flags]
+ public enum FontSimulations : byte
+ {
+ ///
+ /// No simulations are performed.
+ ///
+ None = 0x0000,
+
+ ///
+ /// Algorithmic emboldening is performed.
+ ///
+ Bold = 0x0001,
+
+ ///
+ /// Algorithmic italicization is performed.
+ ///
+ Oblique = 0x0002
+ }
+}
diff --git a/src/Avalonia.Base/Media/FormattedText.cs b/src/Avalonia.Base/Media/FormattedText.cs
index 27d99bdc10..90b9755493 100644
--- a/src/Avalonia.Base/Media/FormattedText.cs
+++ b/src/Avalonia.Base/Media/FormattedText.cs
@@ -93,7 +93,8 @@ namespace Avalonia.Media
runProps,
TextWrapping.WrapWithOverflow,
0, // line height not specified
- 0 // indentation not specified
+ 0, // indentation not specified
+ 0
);
InvalidateMetrics();
diff --git a/src/Avalonia.Base/Media/GlyphMetrics.cs b/src/Avalonia.Base/Media/GlyphMetrics.cs
new file mode 100644
index 0000000000..2ee1f87d38
--- /dev/null
+++ b/src/Avalonia.Base/Media/GlyphMetrics.cs
@@ -0,0 +1,24 @@
+namespace Avalonia.Media;
+
+public readonly struct GlyphMetrics
+{
+ ///
+ /// Distance from the x-origin to the left extremum of the glyph.
+ ///
+ public int XBearing { get; init; }
+
+ ///
+ /// Distance from the top extremum of the glyph to the y-origin.
+ ///
+ public int YBearing{ get; init; }
+
+ ///
+ /// Distance from the left extremum of the glyph to the right extremum.
+ ///
+ public int Width{ get; init; }
+
+ ///
+ /// Distance from the top extremum of the glyph to the bottom extremum.
+ ///
+ public int Height{ get; init; }
+}
diff --git a/src/Avalonia.Base/Media/GlyphRun.cs b/src/Avalonia.Base/Media/GlyphRun.cs
index a1cb00e209..d93a68e78b 100644
--- a/src/Avalonia.Base/Media/GlyphRun.cs
+++ b/src/Avalonia.Base/Media/GlyphRun.cs
@@ -170,7 +170,7 @@ namespace Avalonia.Media
}
///
- /// Gets the scale of the current
+ /// Gets the scale of the current
///
internal double Scale => FontRenderingEmSize / GlyphTypeface.Metrics.DesignEmHeight;
@@ -860,82 +860,9 @@ namespace Avalonia.Media
private IGlyphRunImpl CreateGlyphRunImpl()
{
- IGlyphRunImpl glyphRunImpl;
-
var platformRenderInterface = AvaloniaLocator.Current.GetRequiredService();
- var count = GlyphIndices.Count;
- var scale = (float)(FontRenderingEmSize / GlyphTypeface.Metrics.DesignEmHeight);
-
- if (GlyphOffsets == null)
- {
- if (GlyphTypeface.Metrics.IsFixedPitch)
- {
- var buffer = platformRenderInterface.AllocateGlyphRun(GlyphTypeface, (float)FontRenderingEmSize, count);
-
- var glyphs = buffer.GlyphIndices;
-
- for (int i = 0; i < glyphs.Length; i++)
- {
- glyphs[i] = GlyphIndices[i];
- }
-
- glyphRunImpl = buffer.Build();
- }
- else
- {
- var buffer = platformRenderInterface.AllocateHorizontalGlyphRun(GlyphTypeface, (float)FontRenderingEmSize, count);
- var glyphs = buffer.GlyphIndices;
- var positions = buffer.GlyphPositions;
- var width = 0d;
-
- for (var i = 0; i < count; i++)
- {
- positions[i] = (float)width;
-
- if (GlyphAdvances == null)
- {
- width += GlyphTypeface.GetGlyphAdvance(GlyphIndices[i]) * scale;
- }
- else
- {
- width += GlyphAdvances[i];
- }
-
- glyphs[i] = GlyphIndices[i];
- }
-
- glyphRunImpl = buffer.Build();
- }
- }
- else
- {
- var buffer = platformRenderInterface.AllocatePositionedGlyphRun(GlyphTypeface, (float)FontRenderingEmSize, count);
- var glyphs = buffer.GlyphIndices;
- var glyphPositions = buffer.GlyphPositions;
- var currentX = 0.0;
-
- for (var i = 0; i < count; i++)
- {
- var glyphOffset = GlyphOffsets[i];
-
- glyphPositions[i] = new PointF((float)(currentX + glyphOffset.X), (float)glyphOffset.Y);
-
- if (GlyphAdvances == null)
- {
- currentX += GlyphTypeface.GetGlyphAdvance(GlyphIndices[i]) * scale;
- }
- else
- {
- currentX += GlyphAdvances[i];
- }
-
- glyphs[i] = GlyphIndices[i];
- }
-
- glyphRunImpl = buffer.Build();
- }
- return glyphRunImpl;
+ return platformRenderInterface.CreateGlyphRun(GlyphTypeface, FontRenderingEmSize, GlyphIndices, GlyphAdvances, GlyphOffsets);
}
void IDisposable.Dispose()
diff --git a/src/Avalonia.Base/Media/IGlyphTypeface.cs b/src/Avalonia.Base/Media/IGlyphTypeface.cs
index de2a2309ee..9e1e52cb73 100644
--- a/src/Avalonia.Base/Media/IGlyphTypeface.cs
+++ b/src/Avalonia.Base/Media/IGlyphTypeface.cs
@@ -19,6 +19,21 @@ namespace Avalonia.Media
///
FontMetrics Metrics { get; }
+ ///
+ /// Gets the algorithmic style simulations applied to this glyph typeface.
+ ///
+ FontSimulations FontSimulations { get; }
+
+ ///
+ /// Tries to get a glyph's metrics in em units.
+ ///
+ /// The glyph id.
+ /// The glyph metrics.
+ ///
+ /// true if an glyph's metrics was found, false otherwise.
+ ///
+ bool TryGetGlyphMetrics(ushort glyph, out GlyphMetrics metrics);
+
///
/// Returns an glyph index for the specified codepoint.
///
diff --git a/src/Avalonia.Base/Media/Imaging/Bitmap.cs b/src/Avalonia.Base/Media/Imaging/Bitmap.cs
index cf8a31c3e9..ce38fc5abc 100644
--- a/src/Avalonia.Base/Media/Imaging/Bitmap.cs
+++ b/src/Avalonia.Base/Media/Imaging/Bitmap.cs
@@ -121,18 +121,28 @@ namespace Avalonia.Media.Imaging
/// Saves the bitmap to a file.
///
/// The filename.
- public void Save(string fileName)
+ ///
+ /// The optional quality for compression.
+ /// The quality value is interpreted from 0 - 100. If quality is null the default quality
+ /// setting is applied.
+ ///
+ public void Save(string fileName, int? quality = null)
{
- PlatformImpl.Item.Save(fileName);
+ PlatformImpl.Item.Save(fileName, quality);
}
///
/// Saves the bitmap to a stream.
///
/// The stream.
- public void Save(Stream stream)
+ ///
+ /// The optional quality for compression.
+ /// The quality value is interpreted from 0 - 100. If quality is null the default quality
+ /// setting is applied.
+ ///
+ public void Save(Stream stream, int? quality = null)
{
- PlatformImpl.Item.Save(stream);
+ PlatformImpl.Item.Save(stream, quality);
}
///
diff --git a/src/Avalonia.Base/Media/Imaging/IBitmap.cs b/src/Avalonia.Base/Media/Imaging/IBitmap.cs
index bd04d5ce86..e7d1862aa2 100644
--- a/src/Avalonia.Base/Media/Imaging/IBitmap.cs
+++ b/src/Avalonia.Base/Media/Imaging/IBitmap.cs
@@ -35,12 +35,22 @@ namespace Avalonia.Media.Imaging
/// Saves the bitmap to a file.
///
/// The filename.
- void Save(string fileName);
+ ///
+ /// The optional quality for compression if supported by the specific backend.
+ /// The quality value is interpreted from 0 - 100. If quality is null the default quality
+ /// setting of the backend is applied.
+ ///
+ void Save(string fileName, int? quality = null);
///
/// Saves the bitmap to a stream in png format.
///
/// The stream.
- void Save(Stream stream);
+ ///
+ /// The optional quality for compression if supported by the specific backend.
+ /// The quality value is interpreted from 0 - 100. If quality is null the default quality
+ /// setting of the backend is applied.
+ ///
+ void Save(Stream stream, int? quality = null);
}
}
diff --git a/src/Avalonia.Base/Media/PathMarkupParser.cs b/src/Avalonia.Base/Media/PathMarkupParser.cs
index cd11f9529b..7b9fdf9330 100644
--- a/src/Avalonia.Base/Media/PathMarkupParser.cs
+++ b/src/Avalonia.Base/Media/PathMarkupParser.cs
@@ -188,7 +188,11 @@ namespace Avalonia.Media
_isOpen = true;
}
- private void SetFillRule(scoped ref ReadOnlySpan span)
+ private void SetFillRule(
+#if NET7SDK
+ scoped
+#endif
+ ref ReadOnlySpan span)
{
ThrowIfDisposed();
@@ -452,7 +456,11 @@ namespace Avalonia.Media
return !span.IsEmpty && (span[0] == ',' || span[0] == '-' || span[0] == '.' || char.IsDigit(span[0]));
}
- private static bool ReadArgument(scoped ref ReadOnlySpan remaining, out ReadOnlySpan argument)
+ private static bool ReadArgument(
+#if NET7SDK
+ scoped
+#endif
+ ref ReadOnlySpan remaining, out ReadOnlySpan argument)
{
remaining = SkipWhitespace(remaining);
if (remaining.IsEmpty)
diff --git a/src/Avalonia.Base/Media/TextFormatting/GenericTextParagraphProperties.cs b/src/Avalonia.Base/Media/TextFormatting/GenericTextParagraphProperties.cs
index dccad1e647..b9ed31523e 100644
--- a/src/Avalonia.Base/Media/TextFormatting/GenericTextParagraphProperties.cs
+++ b/src/Avalonia.Base/Media/TextFormatting/GenericTextParagraphProperties.cs
@@ -17,15 +17,18 @@
/// logical horizontal alignment
/// text wrap option
/// Paragraph line height
+ /// letter spacing
public GenericTextParagraphProperties(TextRunProperties defaultTextRunProperties,
TextAlignment textAlignment = TextAlignment.Left,
TextWrapping textWrap = TextWrapping.NoWrap,
- double lineHeight = 0)
+ double lineHeight = 0,
+ double letterSpacing = 0)
{
DefaultTextRunProperties = defaultTextRunProperties;
_textAlignment = textAlignment;
_textWrap = textWrap;
_lineHeight = lineHeight;
+ LetterSpacing = letterSpacing;
}
///
@@ -39,6 +42,7 @@
/// text wrap option
/// Paragraph line height
/// line indentation
+ /// letter spacing
public GenericTextParagraphProperties(
FlowDirection flowDirection,
TextAlignment textAlignment,
@@ -47,8 +51,8 @@
TextRunProperties defaultTextRunProperties,
TextWrapping textWrap,
double lineHeight,
- double indent
- )
+ double indent,
+ double letterSpacing)
{
_flowDirection = flowDirection;
_textAlignment = textAlignment;
@@ -57,6 +61,7 @@
DefaultTextRunProperties = defaultTextRunProperties;
_textWrap = textWrap;
_lineHeight = lineHeight;
+ LetterSpacing = letterSpacing;
Indent = indent;
}
@@ -72,7 +77,8 @@
textParagraphProperties.DefaultTextRunProperties,
textParagraphProperties.TextWrapping,
textParagraphProperties.LineHeight,
- textParagraphProperties.Indent)
+ textParagraphProperties.Indent,
+ textParagraphProperties.LetterSpacing)
{
}
@@ -131,6 +137,11 @@
///
public override double Indent { get; }
+ ///
+ /// The letter spacing
+ ///
+ public override double LetterSpacing { get; }
+
///
/// Set text flow direction
///
diff --git a/src/Avalonia.Base/Media/TextFormatting/TextFormatterImpl.cs b/src/Avalonia.Base/Media/TextFormatting/TextFormatterImpl.cs
index 73dd3366aa..7bad95c4a2 100644
--- a/src/Avalonia.Base/Media/TextFormatting/TextFormatterImpl.cs
+++ b/src/Avalonia.Base/Media/TextFormatting/TextFormatterImpl.cs
@@ -249,7 +249,8 @@ namespace Avalonia.Media.TextFormatting
var shaperOptions = new TextShaperOptions(currentRun.Properties!.Typeface.GlyphTypeface,
currentRun.Properties.FontRenderingEmSize,
- shapeableRun.BidiLevel, currentRun.Properties.CultureInfo, paragraphProperties.DefaultIncrementalTab);
+ shapeableRun.BidiLevel, currentRun.Properties.CultureInfo,
+ paragraphProperties.DefaultIncrementalTab, paragraphProperties.LetterSpacing);
drawableTextRuns.AddRange(ShapeTogether(groupedRuns, text, shaperOptions));
@@ -477,32 +478,35 @@ namespace Avalonia.Media.TextFormatting
{
case ShapedTextCharacters shapedTextCharacters:
{
- var firstCluster = shapedTextCharacters.ShapedBuffer.GlyphClusters[0];
- var lastCluster = firstCluster;
-
- for (var i = 0; i < shapedTextCharacters.ShapedBuffer.Length; i++)
+ if(shapedTextCharacters.ShapedBuffer.Length > 0)
{
- var glyphInfo = shapedTextCharacters.ShapedBuffer[i];
+ var firstCluster = shapedTextCharacters.ShapedBuffer.GlyphClusters[0];
+ var lastCluster = firstCluster;
- if (currentWidth + glyphInfo.GlyphAdvance > paragraphWidth)
+ for (var i = 0; i < shapedTextCharacters.ShapedBuffer.Length; i++)
{
- measuredLength += Math.Max(0, lastCluster - firstCluster);
+ var glyphInfo = shapedTextCharacters.ShapedBuffer[i];
- goto found;
- }
+ if (currentWidth + glyphInfo.GlyphAdvance > paragraphWidth)
+ {
+ measuredLength += Math.Max(0, lastCluster - firstCluster);
- lastCluster = glyphInfo.GlyphCluster;
- currentWidth += glyphInfo.GlyphAdvance;
- }
+ goto found;
+ }
- measuredLength += currentRun.TextSourceLength;
+ lastCluster = glyphInfo.GlyphCluster;
+ currentWidth += glyphInfo.GlyphAdvance;
+ }
+
+ measuredLength += currentRun.TextSourceLength;
+ }
break;
}
case { } drawableTextRun:
{
- if (currentWidth + drawableTextRun.Size.Width > paragraphWidth)
+ if (currentWidth + drawableTextRun.Size.Width >= paragraphWidth)
{
goto found;
}
@@ -665,7 +669,7 @@ namespace Avalonia.Media.TextFormatting
if (!breakFound)
{
- currentLength += currentRun.Text.Length;
+ currentLength += currentRun.TextSourceLength;
continue;
}
diff --git a/src/Avalonia.Base/Media/TextFormatting/TextLayout.cs b/src/Avalonia.Base/Media/TextFormatting/TextLayout.cs
index 0828b6518a..dc79e61333 100644
--- a/src/Avalonia.Base/Media/TextFormatting/TextLayout.cs
+++ b/src/Avalonia.Base/Media/TextFormatting/TextLayout.cs
@@ -31,6 +31,7 @@ namespace Avalonia.Media.TextFormatting
/// The maximum width.
/// The maximum height.
/// The height of each line of text.
+ /// The letter spacing that is applied to rendered glyphs.
/// The maximum number of text lines.
/// The text style overrides.
public TextLayout(
@@ -46,12 +47,13 @@ namespace Avalonia.Media.TextFormatting
double maxWidth = double.PositiveInfinity,
double maxHeight = double.PositiveInfinity,
double lineHeight = double.NaN,
+ double letterSpacing = 0,
int maxLines = 0,
IReadOnlyList>? textStyleOverrides = null)
{
_paragraphProperties =
CreateTextParagraphProperties(typeface, fontSize, foreground, textAlignment, textWrapping,
- textDecorations, flowDirection, lineHeight);
+ textDecorations, flowDirection, lineHeight, letterSpacing);
_textSource = new FormattedTextSource(text.AsMemory(), _paragraphProperties.DefaultTextRunProperties, textStyleOverrides);
@@ -63,6 +65,8 @@ namespace Avalonia.Media.TextFormatting
MaxHeight = maxHeight;
+ LetterSpacing = letterSpacing;
+
MaxLines = maxLines;
TextLines = CreateTextLines();
@@ -77,6 +81,7 @@ namespace Avalonia.Media.TextFormatting
/// The maximum width.
/// The maximum height.
/// The height of each line of text.
+ /// The letter spacing that is applied to rendered glyphs.
/// The maximum number of text lines.
public TextLayout(
ITextSource textSource,
@@ -85,6 +90,7 @@ namespace Avalonia.Media.TextFormatting
double maxWidth = double.PositiveInfinity,
double maxHeight = double.PositiveInfinity,
double lineHeight = double.NaN,
+ double letterSpacing = 0,
int maxLines = 0)
{
_textSource = textSource;
@@ -99,6 +105,8 @@ namespace Avalonia.Media.TextFormatting
MaxHeight = maxHeight;
+ LetterSpacing = letterSpacing;
+
MaxLines = maxLines;
TextLines = CreateTextLines();
@@ -128,6 +136,11 @@ namespace Avalonia.Media.TextFormatting
///
public int MaxLines { get; }
+ ///
+ /// Gets the text spacing.
+ ///
+ public double LetterSpacing { get; }
+
///
/// Gets the text lines.
///
@@ -374,15 +387,17 @@ namespace Avalonia.Media.TextFormatting
/// The text decorations.
/// The text flow direction.
/// The height of each line of text.
+ /// The letter spacing that is applied to rendered glyphs.
///
private static TextParagraphProperties CreateTextParagraphProperties(Typeface typeface, double fontSize,
IBrush? foreground, TextAlignment textAlignment, TextWrapping textWrapping,
- TextDecorationCollection? textDecorations, FlowDirection flowDirection, double lineHeight)
+ TextDecorationCollection? textDecorations, FlowDirection flowDirection, double lineHeight,
+ double letterSpacing)
{
var textRunStyle = new GenericTextRunProperties(typeface, fontSize, textDecorations, foreground);
return new GenericTextParagraphProperties(flowDirection, textAlignment, true, false,
- textRunStyle, textWrapping, lineHeight, 0);
+ textRunStyle, textWrapping, lineHeight, 0, letterSpacing);
}
///
diff --git a/src/Avalonia.Base/Media/TextFormatting/TextParagraphProperties.cs b/src/Avalonia.Base/Media/TextFormatting/TextParagraphProperties.cs
index 82a0ba14d8..5691dd8ad0 100644
--- a/src/Avalonia.Base/Media/TextFormatting/TextParagraphProperties.cs
+++ b/src/Avalonia.Base/Media/TextFormatting/TextParagraphProperties.cs
@@ -57,7 +57,7 @@
public abstract double Indent { get; }
///
- /// Paragraph indentation
+ /// Get the paragraph indentation.
///
public virtual double ParagraphIndent
{
@@ -65,11 +65,16 @@
}
///
- /// Default Incremental Tab
+ /// Gets the default incremental tab width.
///
public virtual double DefaultIncrementalTab
{
get { return 4 * DefaultTextRunProperties.FontRenderingEmSize; }
}
+
+ ///
+ /// Gets the letter spacing.
+ ///
+ public virtual double LetterSpacing { get; }
}
}
diff --git a/src/Avalonia.Base/Media/TextFormatting/TextShaperOptions.cs b/src/Avalonia.Base/Media/TextFormatting/TextShaperOptions.cs
index 0d00bed51e..80bbbcdbfe 100644
--- a/src/Avalonia.Base/Media/TextFormatting/TextShaperOptions.cs
+++ b/src/Avalonia.Base/Media/TextFormatting/TextShaperOptions.cs
@@ -12,13 +12,15 @@ namespace Avalonia.Media.TextFormatting
double fontRenderingEmSize = 12,
sbyte bidiLevel = 0,
CultureInfo? culture = null,
- double incrementalTabWidth = 0)
+ double incrementalTabWidth = 0,
+ double letterSpacing = 0)
{
Typeface = typeface;
FontRenderingEmSize = fontRenderingEmSize;
BidiLevel = bidiLevel;
Culture = culture;
IncrementalTabWidth = incrementalTabWidth;
+ LetterSpacing = letterSpacing;
}
///
@@ -45,5 +47,10 @@ namespace Avalonia.Media.TextFormatting
///
public double IncrementalTabWidth { get; }
+ ///
+ /// Get the letter spacing.
+ ///
+ public double LetterSpacing { get; }
+
}
}
diff --git a/src/Avalonia.Base/Platform/IBitmapImpl.cs b/src/Avalonia.Base/Platform/IBitmapImpl.cs
index 8f11f68e7c..299a758961 100644
--- a/src/Avalonia.Base/Platform/IBitmapImpl.cs
+++ b/src/Avalonia.Base/Platform/IBitmapImpl.cs
@@ -29,12 +29,22 @@ namespace Avalonia.Platform
/// Saves the bitmap to a file.
///
/// The filename.
- void Save(string fileName);
+ ///
+ /// The optional quality for compression if supported by the specific backend.
+ /// The quality value is interpreted from 0 - 100. If quality is null the default quality
+ /// setting of the backend is applied.
+ ///
+ void Save(string fileName, int? quality = null);
///
/// Saves the bitmap to a stream in png format.
///
/// The stream.
- void Save(Stream stream);
+ ///
+ /// The optional quality for compression if supported by the specific backend.
+ /// The quality value is interpreted from 0 - 100. If quality is null the default quality
+ /// setting of the backend is applied.
+ ///
+ void Save(Stream stream, int? quality = null);
}
}
diff --git a/src/Avalonia.Base/Platform/IPlatformRenderInterface.cs b/src/Avalonia.Base/Platform/IPlatformRenderInterface.cs
index 9d0d7974b4..518c5f37b8 100644
--- a/src/Avalonia.Base/Platform/IPlatformRenderInterface.cs
+++ b/src/Avalonia.Base/Platform/IPlatformRenderInterface.cs
@@ -171,40 +171,15 @@ namespace Avalonia.Platform
IBitmapImpl LoadBitmap(PixelFormat format, AlphaFormat alphaFormat, IntPtr data, PixelSize size, Vector dpi, int stride);
///
- /// Allocates a platform glyph run buffer.
+ /// Creates a platform implementation of a glyph run.
///
/// The glyph typeface.
/// The font rendering em size.
- /// The length.
- /// An .
- ///
- /// This buffer only holds glyph indices.
- ///
- IGlyphRunBuffer AllocateGlyphRun(IGlyphTypeface glyphTypeface, float fontRenderingEmSize, int length);
-
- ///
- /// Allocates a horizontal platform glyph run buffer.
- ///
- /// The glyph typeface.
- /// The font rendering em size.
- /// The length.
- /// An .
- ///
- /// This buffer holds glyph indices and glyph advances.
- ///
- IHorizontalGlyphRunBuffer AllocateHorizontalGlyphRun(IGlyphTypeface glyphTypeface, float fontRenderingEmSize, int length);
-
- ///
- /// Allocates a positioned platform glyph run buffer.
- ///
- /// The glyph typeface.
- /// The font rendering em size.
- /// The length.
- /// An .
- ///
- /// This buffer holds glyph indices, glyph advances and glyph positions.
- ///
- IPositionedGlyphRunBuffer AllocatePositionedGlyphRun(IGlyphTypeface glyphTypeface, float fontRenderingEmSize, int length);
+ /// The glyph indices.
+ /// The glyph advances.
+ /// The glyph offsets.
+ ///
+ IGlyphRunImpl CreateGlyphRun(IGlyphTypeface glyphTypeface, double fontRenderingEmSize, IReadOnlyList glyphIndices, IReadOnlyList? glyphAdvances, IReadOnlyList? glyphOffsets);
///
/// Gets a value indicating whether the platform directly supports rectangles with rounded corners.
diff --git a/src/Avalonia.Base/Rendering/SceneInvalidatedEventArgs.cs b/src/Avalonia.Base/Rendering/SceneInvalidatedEventArgs.cs
index cac4d1693a..73840376fe 100644
--- a/src/Avalonia.Base/Rendering/SceneInvalidatedEventArgs.cs
+++ b/src/Avalonia.Base/Rendering/SceneInvalidatedEventArgs.cs
@@ -12,7 +12,7 @@ namespace Avalonia.Rendering
///
/// The render root that has been updated.
/// The updated area.
- public SceneInvalidatedEventArgs(
+ internal SceneInvalidatedEventArgs(
IRenderRoot root,
Rect dirtyRect)
{
diff --git a/src/Avalonia.Base/Utilities/IdentifierParser.cs b/src/Avalonia.Base/Utilities/IdentifierParser.cs
index ee176a6b85..76e6459e2e 100644
--- a/src/Avalonia.Base/Utilities/IdentifierParser.cs
+++ b/src/Avalonia.Base/Utilities/IdentifierParser.cs
@@ -8,7 +8,11 @@ namespace Avalonia.Utilities
#endif
static class IdentifierParser
{
- public static ReadOnlySpan ParseIdentifier(this scoped ref CharacterReader r)
+ public static ReadOnlySpan ParseIdentifier(this
+#if NET7SDK
+ scoped
+#endif
+ ref CharacterReader r)
{
if (IsValidIdentifierStart(r.Peek))
{
diff --git a/src/Avalonia.Controls.ColorPicker/ColorPicker/ColorPicker.cs b/src/Avalonia.Controls.ColorPicker/ColorPicker/ColorPicker.cs
index 29f9f3c571..01cb745ba7 100644
--- a/src/Avalonia.Controls.ColorPicker/ColorPicker/ColorPicker.cs
+++ b/src/Avalonia.Controls.ColorPicker/ColorPicker/ColorPicker.cs
@@ -14,33 +14,5 @@ namespace Avalonia.Controls
public ColorPicker() : base()
{
}
-
- ///
- protected override void OnApplyTemplate(TemplateAppliedEventArgs e)
- {
- base.OnApplyTemplate(e);
-
- // Until this point the ColorPicker itself is responsible to process property updates.
- // This, for example, syncs Color with HsvColor and updates primitive controls.
- //
- // However, when the template is created, hand-off this change processing to the
- // ColorView within the control template itself. Remember ColorPicker derives from
- // ColorView so we don't want two instances of the same logic fighting each other.
- // It is best to hand-off to the ColorView in the control template because that is the
- // primary point of user-interaction for the overall control. It also simplifies binding.
- //
- // Keep in mind this hand-off is not possible until the template controls are created
- // which is done after the ColorPicker is instantiated. The ColorPicker must still
- // process updates before the template is applied to ensure all property changes in
- // XAML or object initializers are handled correctly. Otherwise, there can be bugs
- // such as setting the Color property doesn't work because the HsvColor is never updated
- // and then the Color value is lost once the template loads (and the template ColorView
- // takes over).
- //
- // In order to complete this hand-off, completely ignore property changes here in the
- // ColorPicker. This means the ColorView in the control template is now responsible to
- // process property changes and handle primary calculations.
- base.ignorePropertyChanged = true;
- }
}
}
diff --git a/src/Avalonia.Controls.ColorPicker/ColorSpectrum/ColorSpectrum.Properties.cs b/src/Avalonia.Controls.ColorPicker/ColorSpectrum/ColorSpectrum.Properties.cs
index 39b7b7f660..5c7de2459b 100644
--- a/src/Avalonia.Controls.ColorPicker/ColorSpectrum/ColorSpectrum.Properties.cs
+++ b/src/Avalonia.Controls.ColorPicker/ColorSpectrum/ColorSpectrum.Properties.cs
@@ -240,7 +240,7 @@ namespace Avalonia.Controls.Primitives
public ColorComponent ThirdComponent
{
get => GetValue(ThirdComponentProperty);
- private set => SetValue(ThirdComponentProperty, value);
+ protected set => SetValue(ThirdComponentProperty, value);
}
}
}
diff --git a/src/Avalonia.Controls.ColorPicker/Themes/Fluent/ColorPicker.xaml b/src/Avalonia.Controls.ColorPicker/Themes/Fluent/ColorPicker.xaml
index 74a1df4991..1bcd17393d 100644
--- a/src/Avalonia.Controls.ColorPicker/Themes/Fluent/ColorPicker.xaml
+++ b/src/Avalonia.Controls.ColorPicker/Themes/Fluent/ColorPicker.xaml
@@ -1,8 +1,15 @@
+
+
+
+
@@ -43,39 +50,454 @@
-
-
+
+
+
+
+
5,5,0,0
-
-
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/src/Avalonia.Controls.ColorPicker/Themes/Simple/ColorPicker.xaml b/src/Avalonia.Controls.ColorPicker/Themes/Simple/ColorPicker.xaml
index 560d326f92..fdee64dfd2 100644
--- a/src/Avalonia.Controls.ColorPicker/Themes/Simple/ColorPicker.xaml
+++ b/src/Avalonia.Controls.ColorPicker/Themes/Simple/ColorPicker.xaml
@@ -1,8 +1,15 @@
+
+
+
+
@@ -42,40 +49,455 @@
-
-
-
+
+
+
+
+
+
0,0,0,0
-
-
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/src/Avalonia.Controls/Control.cs b/src/Avalonia.Controls/Control.cs
index 524362fcf9..beaee34b07 100644
--- a/src/Avalonia.Controls/Control.cs
+++ b/src/Avalonia.Controls/Control.cs
@@ -84,6 +84,13 @@ namespace Avalonia.Controls
nameof(Unloaded),
RoutingStrategies.Direct);
+ ///
+ /// Defines the event.
+ ///
+ public static readonly RoutedEvent SizeChangedEvent =
+ RoutedEvent.Register(
+ nameof(SizeChanged), RoutingStrategies.Direct);
+
///
/// Defines the property.
///
@@ -211,6 +218,15 @@ namespace Avalonia.Controls
remove => RemoveHandler(UnloadedEvent, value);
}
+ ///
+ /// Occurs when the bounds (actual size) of the control have changed.
+ ///
+ public event EventHandler? SizeChanged
+ {
+ add => AddHandler(SizeChangedEvent, value);
+ remove => RemoveHandler(SizeChangedEvent, value);
+ }
+
public new IControl? Parent => (IControl?)base.Parent;
///
@@ -530,14 +546,35 @@ namespace Avalonia.Controls
}
}
+ ///
protected override void OnPropertyChanged(AvaloniaPropertyChangedEventArgs change)
{
base.OnPropertyChanged(change);
- if (change.Property == FlowDirectionProperty)
+ if (change.Property == BoundsProperty)
+ {
+ var oldValue = change.GetOldValue();
+ var newValue = change.GetNewValue();
+
+ // Bounds is a Rect with an X/Y Position as well as Height/Width.
+ // This means it is possible for the Rect to change position but not size.
+ // Therefore, we want to explicity check only the size and raise an event
+ // only when that size has changed.
+ if (newValue.Size != oldValue.Size)
+ {
+ var sizeChangedEventArgs = new SizeChangedEventArgs(
+ SizeChangedEvent,
+ source: this,
+ previousSize: new Size(oldValue.Width, oldValue.Height),
+ newSize: new Size(newValue.Width, newValue.Height));
+
+ RaiseEvent(sizeChangedEventArgs);
+ }
+ }
+ else if (change.Property == FlowDirectionProperty)
{
InvalidateMirrorTransform();
-
+
foreach (var visual in VisualChildren)
{
if (visual is Control child)
diff --git a/src/Avalonia.Controls/Documents/InlineCollection.cs b/src/Avalonia.Controls/Documents/InlineCollection.cs
index 1ba65b3e8f..a265f88e21 100644
--- a/src/Avalonia.Controls/Documents/InlineCollection.cs
+++ b/src/Avalonia.Controls/Documents/InlineCollection.cs
@@ -12,7 +12,7 @@ namespace Avalonia.Controls.Documents
[WhitespaceSignificantCollection]
public class InlineCollection : AvaloniaList
{
- private ILogical? _parent;
+ private IAvaloniaList? _logicalChildren;
private IInlineHost? _inlineHost;
///
@@ -24,28 +24,30 @@ namespace Avalonia.Controls.Documents
this.ForEachItem(
x =>
- {
- ((ISetLogicalParent)x).SetParent(Parent);
+ {
x.InlineHost = InlineHost;
+ LogicalChildren?.Add(x);
Invalidate();
},
x =>
{
- ((ISetLogicalParent)x).SetParent(null);
+ LogicalChildren?.Remove(x);
x.InlineHost = InlineHost;
Invalidate();
},
() => throw new NotSupportedException());
}
- internal ILogical? Parent
+ internal IAvaloniaList? LogicalChildren
{
- get => _parent;
+ get => _logicalChildren;
set
{
- _parent = value;
+ var oldValue = _logicalChildren;
+
+ _logicalChildren = value;
- OnParentChanged(value);
+ OnParentChanged(oldValue, value);
}
}
@@ -70,6 +72,11 @@ namespace Avalonia.Controls.Documents
{
get
{
+ if (Count == 0)
+ {
+ return null;
+ }
+
var builder = StringBuilderCache.Acquire();
foreach (var inline in this)
@@ -111,7 +118,7 @@ namespace Avalonia.Controls.Documents
private void AddText(string text)
{
- if (Parent is RichTextBlock textBlock && !textBlock.HasComplexContent)
+ if (LogicalChildren is TextBlock textBlock && !textBlock.HasComplexContent)
{
textBlock._text += text;
}
@@ -123,7 +130,7 @@ namespace Avalonia.Controls.Documents
private void OnAdd()
{
- if (Parent is RichTextBlock textBlock)
+ if (LogicalChildren is TextBlock textBlock)
{
if (!textBlock.HasComplexContent && !string.IsNullOrEmpty(textBlock._text))
{
@@ -152,20 +159,21 @@ namespace Avalonia.Controls.Documents
Invalidated?.Invoke(this, EventArgs.Empty);
}
- private void OnParentChanged(ILogical? parent)
+ private void OnParentChanged(IAvaloniaList? oldParent, IAvaloniaList? newParent)
{
foreach (var child in this)
{
- var oldParent = child.Parent;
-
- if (oldParent != parent)
+ if (oldParent != newParent)
{
if (oldParent != null)
{
- ((ISetLogicalParent)child).SetParent(null);
+ oldParent.Remove(child);
}
- ((ISetLogicalParent)child).SetParent(parent);
+ if(newParent != null)
+ {
+ newParent.Add(child);
+ }
}
}
}
diff --git a/src/Avalonia.Controls/Documents/Span.cs b/src/Avalonia.Controls/Documents/Span.cs
index 363ce1011b..a7a702ceae 100644
--- a/src/Avalonia.Controls/Documents/Span.cs
+++ b/src/Avalonia.Controls/Documents/Span.cs
@@ -21,7 +21,7 @@ namespace Avalonia.Controls.Documents
{
Inlines = new InlineCollection
{
- Parent = this
+ LogicalChildren = LogicalChildren
};
}
@@ -78,14 +78,14 @@ namespace Avalonia.Controls.Documents
{
if (oldValue is not null)
{
- oldValue.Parent = null;
+ oldValue.LogicalChildren = null;
oldValue.InlineHost = null;
oldValue.Invalidated -= (s, e) => InlineHost?.Invalidate();
}
if (newValue is not null)
{
- newValue.Parent = this;
+ newValue.LogicalChildren = LogicalChildren;
newValue.InlineHost = InlineHost;
newValue.Invalidated += (s, e) => InlineHost?.Invalidate();
}
diff --git a/src/Avalonia.Controls/ListBox.cs b/src/Avalonia.Controls/ListBox.cs
index 80b5259a53..86118d7b00 100644
--- a/src/Avalonia.Controls/ListBox.cs
+++ b/src/Avalonia.Controls/ListBox.cs
@@ -139,7 +139,8 @@ namespace Avalonia.Controls
e.Source,
true,
e.KeyModifiers.HasAllFlags(KeyModifiers.Shift),
- e.KeyModifiers.HasAllFlags(KeyModifiers.Control));
+ e.KeyModifiers.HasAllFlags(KeyModifiers.Control),
+ fromFocus: true);
}
}
diff --git a/src/Avalonia.Controls/Presenters/TextPresenter.cs b/src/Avalonia.Controls/Presenters/TextPresenter.cs
index a9bb16c7df..adf0569551 100644
--- a/src/Avalonia.Controls/Presenters/TextPresenter.cs
+++ b/src/Avalonia.Controls/Presenters/TextPresenter.cs
@@ -80,6 +80,12 @@ namespace Avalonia.Controls.Presenters
public static readonly StyledProperty LineHeightProperty =
TextBlock.LineHeightProperty.AddOwner();
+ ///
+ /// Defines the property.
+ ///
+ public static readonly StyledProperty LetterSpacingProperty =
+ TextBlock.LetterSpacingProperty.AddOwner();
+
///
/// Defines the property.
///
@@ -212,6 +218,15 @@ namespace Avalonia.Controls.Presenters
set => SetValue(LineHeightProperty, value);
}
+ ///
+ /// Gets or sets the letter spacing.
+ ///
+ public double LetterSpacing
+ {
+ get => GetValue(LetterSpacingProperty);
+ set => SetValue(LetterSpacingProperty, value);
+ }
+
///
/// Gets or sets the text alignment.
///
@@ -333,7 +348,7 @@ namespace Avalonia.Controls.Presenters
var textLayout = new TextLayout(text, typeface, FontSize, foreground, TextAlignment,
TextWrapping, maxWidth: maxWidth, maxHeight: maxHeight, textStyleOverrides: textStyleOverrides,
- flowDirection: FlowDirection, lineHeight: LineHeight);
+ flowDirection: FlowDirection, lineHeight: LineHeight, letterSpacing: LetterSpacing);
return textLayout;
}
@@ -916,6 +931,9 @@ namespace Avalonia.Controls.Presenters
case nameof(TextAlignment):
case nameof(TextWrapping):
+ case nameof(LineHeight):
+ case nameof(LetterSpacing):
+
case nameof(SelectionStart):
case nameof(SelectionEnd):
case nameof(SelectionForegroundBrush):
diff --git a/src/Avalonia.Controls/Primitives/SelectingItemsControl.cs b/src/Avalonia.Controls/Primitives/SelectingItemsControl.cs
index ea20247b4b..e03b02a479 100644
--- a/src/Avalonia.Controls/Primitives/SelectingItemsControl.cs
+++ b/src/Avalonia.Controls/Primitives/SelectingItemsControl.cs
@@ -586,6 +586,14 @@ namespace Avalonia.Controls.Primitives
Selection.SelectAll();
e.Handled = true;
}
+ else if (e.Key == Key.Space || e.Key == Key.Enter)
+ {
+ e.Handled = UpdateSelectionFromEventSource(
+ e.Source,
+ true,
+ e.KeyModifiers.HasFlag(KeyModifiers.Shift),
+ e.KeyModifiers.HasFlag(KeyModifiers.Control));
+ }
}
}
@@ -662,12 +670,14 @@ namespace Avalonia.Controls.Primitives
/// Whether the range modifier is enabled (i.e. shift key).
/// Whether the toggle modifier is enabled (i.e. ctrl key).
/// Whether the event is a right-click.
+ /// Wheter the event is a focus event
protected void UpdateSelection(
int index,
bool select = true,
bool rangeModifier = false,
bool toggleModifier = false,
- bool rightButton = false)
+ bool rightButton = false,
+ bool fromFocus = false)
{
if (index < 0 || index >= ItemCount)
{
@@ -696,22 +706,25 @@ namespace Avalonia.Controls.Primitives
Selection.Clear();
Selection.SelectRange(Selection.AnchorIndex, index);
}
- else if (multi && toggle)
+ else if (!fromFocus && toggle)
{
- if (Selection.IsSelected(index) == true)
+ if (multi)
{
- Selection.Deselect(index);
+ if (Selection.IsSelected(index) == true)
+ {
+ Selection.Deselect(index);
+ }
+ else
+ {
+ Selection.Select(index);
+ }
}
else
{
- Selection.Select(index);
+ SelectedIndex = (SelectedIndex == index) ? -1 : index;
}
}
- else if (toggle)
- {
- SelectedIndex = (SelectedIndex == index) ? -1 : index;
- }
- else
+ else if (!toggle)
{
using var operation = Selection.BatchUpdate();
Selection.Clear();
@@ -735,18 +748,20 @@ namespace Avalonia.Controls.Primitives
/// Whether the range modifier is enabled (i.e. shift key).
/// Whether the toggle modifier is enabled (i.e. ctrl key).
/// Whether the event is a right-click.
+ /// Wheter the event is a focus event
protected void UpdateSelection(
IControl container,
bool select = true,
bool rangeModifier = false,
bool toggleModifier = false,
- bool rightButton = false)
+ bool rightButton = false,
+ bool fromFocus = false)
{
var index = ItemContainerGenerator?.IndexFromContainer(container) ?? -1;
if (index != -1)
{
- UpdateSelection(index, select, rangeModifier, toggleModifier, rightButton);
+ UpdateSelection(index, select, rangeModifier, toggleModifier, rightButton, fromFocus);
}
}
@@ -759,6 +774,7 @@ namespace Avalonia.Controls.Primitives
/// Whether the range modifier is enabled (i.e. shift key).
/// Whether the toggle modifier is enabled (i.e. ctrl key).
/// Whether the event is a right-click.
+ /// Wheter the event is a focus event
///
/// True if the event originated from a container that belongs to the control; otherwise
/// false.
@@ -768,13 +784,14 @@ namespace Avalonia.Controls.Primitives
bool select = true,
bool rangeModifier = false,
bool toggleModifier = false,
- bool rightButton = false)
+ bool rightButton = false,
+ bool fromFocus = false)
{
var container = GetContainerFromEventSource(eventSource);
if (container != null)
{
- UpdateSelection(container, select, rangeModifier, toggleModifier, rightButton);
+ UpdateSelection(container, select, rangeModifier, toggleModifier, rightButton, fromFocus);
return true;
}
diff --git a/src/Avalonia.Controls/RichTextBlock.cs b/src/Avalonia.Controls/SelectableTextBlock.cs
similarity index 55%
rename from src/Avalonia.Controls/RichTextBlock.cs
rename to src/Avalonia.Controls/SelectableTextBlock.cs
index d0b713ba56..b343439f98 100644
--- a/src/Avalonia.Controls/RichTextBlock.cs
+++ b/src/Avalonia.Controls/SelectableTextBlock.cs
@@ -8,7 +8,6 @@ using Avalonia.Input.Platform;
using Avalonia.Interactivity;
using Avalonia.Media;
using Avalonia.Media.TextFormatting;
-using Avalonia.Metadata;
using Avalonia.Utilities;
namespace Avalonia.Controls
@@ -16,67 +15,53 @@ namespace Avalonia.Controls
///
/// A control that displays a block of formatted text.
///
- public class RichTextBlock : TextBlock, IInlineHost
+ public class SelectableTextBlock : TextBlock, IInlineHost
{
- public static readonly StyledProperty IsTextSelectionEnabledProperty =
- AvaloniaProperty.Register(nameof(IsTextSelectionEnabled), false);
-
- public static readonly DirectProperty SelectionStartProperty =
- AvaloniaProperty.RegisterDirect(
+ public static readonly DirectProperty SelectionStartProperty =
+ AvaloniaProperty.RegisterDirect(
nameof(SelectionStart),
o => o.SelectionStart,
(o, v) => o.SelectionStart = v);
- public static readonly DirectProperty SelectionEndProperty =
- AvaloniaProperty.RegisterDirect(
+ public static readonly DirectProperty SelectionEndProperty =
+ AvaloniaProperty.RegisterDirect(
nameof(SelectionEnd),
o => o.SelectionEnd,
(o, v) => o.SelectionEnd = v);
- public static readonly DirectProperty SelectedTextProperty =
- AvaloniaProperty.RegisterDirect(
+ public static readonly DirectProperty SelectedTextProperty =
+ AvaloniaProperty.RegisterDirect(
nameof(SelectedText),
o => o.SelectedText);
public static readonly StyledProperty SelectionBrushProperty =
- AvaloniaProperty.Register(nameof(SelectionBrush), Brushes.Blue);
+ AvaloniaProperty.Register(nameof(SelectionBrush), Brushes.Blue);
- ///
- /// Defines the property.
- ///
- public static readonly StyledProperty InlinesProperty =
- AvaloniaProperty.Register(
- nameof(Inlines));
- public static readonly DirectProperty CanCopyProperty =
- AvaloniaProperty.RegisterDirect(
+ public static readonly DirectProperty CanCopyProperty =
+ AvaloniaProperty.RegisterDirect(
nameof(CanCopy),
o => o.CanCopy);
public static readonly RoutedEvent CopyingToClipboardEvent =
- RoutedEvent.Register(
+ RoutedEvent.Register(
nameof(CopyingToClipboard), RoutingStrategies.Bubble);
private bool _canCopy;
private int _selectionStart;
private int _selectionEnd;
private int _wordSelectionStart = -1;
- private IReadOnlyList? _textRuns;
- static RichTextBlock()
+ static SelectableTextBlock()
{
- FocusableProperty.OverrideDefaultValue(typeof(RichTextBlock), true);
-
- AffectsRender(SelectionStartProperty, SelectionEndProperty, SelectionBrushProperty, IsTextSelectionEnabledProperty);
+ FocusableProperty.OverrideDefaultValue(typeof(SelectableTextBlock), true);
+ AffectsRender(SelectionStartProperty, SelectionEndProperty, SelectionBrushProperty);
}
- public RichTextBlock()
+ public event EventHandler? CopyingToClipboard
{
- Inlines = new InlineCollection
- {
- Parent = this,
- InlineHost = this
- };
+ add => AddHandler(CopyingToClipboardEvent, value);
+ remove => RemoveHandler(CopyingToClipboardEvent, value);
}
///
@@ -99,6 +84,8 @@ namespace Avalonia.Controls
if (SetAndRaise(SelectionStartProperty, ref _selectionStart, value))
{
RaisePropertyChanged(SelectedTextProperty, "", "");
+
+ UpdateCommandStates();
}
}
}
@@ -114,6 +101,8 @@ namespace Avalonia.Controls
if (SetAndRaise(SelectionEndProperty, ref _selectionEnd, value))
{
RaisePropertyChanged(SelectedTextProperty, "", "");
+
+ UpdateCommandStates();
}
}
}
@@ -126,25 +115,6 @@ namespace Avalonia.Controls
get => GetSelection();
}
- ///
- /// Gets or sets a value that indicates whether text selection is enabled, either through user action or calling selection-related API.
- ///
- public bool IsTextSelectionEnabled
- {
- get => GetValue(IsTextSelectionEnabledProperty);
- set => SetValue(IsTextSelectionEnabledProperty, value);
- }
-
- ///
- /// Gets or sets the inlines.
- ///
- [Content]
- public InlineCollection? Inlines
- {
- get => GetValue(InlinesProperty);
- set => SetValue(InlinesProperty, value);
- }
-
///
/// Property for determining if the Copy command can be executed.
///
@@ -154,20 +124,12 @@ namespace Avalonia.Controls
private set => SetAndRaise(CanCopyProperty, ref _canCopy, value);
}
- public event EventHandler? CopyingToClipboard
- {
- add => AddHandler(CopyingToClipboardEvent, value);
- remove => RemoveHandler(CopyingToClipboardEvent, value);
- }
-
- internal bool HasComplexContent => Inlines != null && Inlines.Count > 0;
-
///
/// Copies the current selection to the Clipboard.
///
public async void Copy()
{
- if (_canCopy || !IsTextSelectionEnabled)
+ if (!_canCopy)
{
return;
}
@@ -188,45 +150,13 @@ namespace Avalonia.Controls
await ((IClipboard)AvaloniaLocator.Current.GetRequiredService(typeof(IClipboard)))
.SetTextAsync(text);
}
- }
-
- protected override void RenderTextLayout(DrawingContext context, Point origin)
- {
- var selectionStart = SelectionStart;
- var selectionEnd = SelectionEnd;
- var selectionBrush = SelectionBrush;
-
- var selectionEnabled = IsTextSelectionEnabled;
-
- if (selectionEnabled && selectionStart != selectionEnd && selectionBrush != null)
- {
- var start = Math.Min(selectionStart, selectionEnd);
- var length = Math.Max(selectionStart, selectionEnd) - start;
-
- var rects = TextLayout.HitTestTextRange(start, length);
-
- using (context.PushPostTransform(Matrix.CreateTranslation(origin)))
- {
- foreach (var rect in rects)
- {
- context.FillRectangle(selectionBrush, PixelRect.FromRect(rect, 1).ToRect(1));
- }
- }
- }
-
- base.RenderTextLayout(context, origin);
- }
+ }
///
/// Select all text in the TextBox
///
public void SelectAll()
{
- if (!IsTextSelectionEnabled)
- {
- return;
- }
-
var text = Text;
SelectionStart = 0;
@@ -238,94 +168,52 @@ namespace Avalonia.Controls
///
public void ClearSelection()
{
- if (!IsTextSelectionEnabled)
- {
- return;
- }
-
SelectionEnd = SelectionStart;
}
- protected void AddText(string? text)
+ protected override void OnGotFocus(GotFocusEventArgs e)
{
- if (string.IsNullOrEmpty(text))
- {
- return;
- }
-
- if (!HasComplexContent && string.IsNullOrEmpty(_text))
- {
- _text = text;
- }
- else
- {
- if (!string.IsNullOrEmpty(_text))
- {
- Inlines?.Add(_text);
-
- _text = null;
- }
-
- Inlines?.Add(text);
- }
- }
+ base.OnGotFocus(e);
- protected override string? GetText()
- {
- return _text ?? Inlines?.Text;
+ UpdateCommandStates();
}
- protected override void SetText(string? text)
+ protected override void OnLostFocus(RoutedEventArgs e)
{
- var oldValue = GetText();
+ base.OnLostFocus(e);
- AddText(text);
+ if ((ContextFlyout == null || !ContextFlyout.IsOpen) &&
+ (ContextMenu == null || !ContextMenu.IsOpen))
+ {
+ ClearSelection();
+ }
- RaisePropertyChanged(TextProperty, oldValue, text);
+ UpdateCommandStates();
}
- ///
- /// Creates the used to render the text.
- ///
- /// A object.
- protected override TextLayout CreateTextLayout(string? text)
+ protected override void RenderTextLayout(DrawingContext context, Point origin)
{
- var typeface = new Typeface(FontFamily, FontStyle, FontWeight, FontStretch);
- var defaultProperties = new GenericTextRunProperties(
- typeface,
- FontSize,
- TextDecorations,
- Foreground);
-
- var paragraphProperties = new GenericTextParagraphProperties(FlowDirection, TextAlignment, true, false,
- defaultProperties, TextWrapping, LineHeight, 0);
-
- ITextSource textSource;
+ var selectionStart = SelectionStart;
+ var selectionEnd = SelectionEnd;
+ var selectionBrush = SelectionBrush;
- if (_textRuns != null)
+ if (selectionStart != selectionEnd && selectionBrush != null)
{
- textSource = new InlinesTextSource(_textRuns);
- }
- else
- {
- textSource = new SimpleTextSource((text ?? "").AsMemory(), defaultProperties);
- }
+ var start = Math.Min(selectionStart, selectionEnd);
+ var length = Math.Max(selectionStart, selectionEnd) - start;
- return new TextLayout(
- textSource,
- paragraphProperties,
- TextTrimming,
- _constraint.Width,
- _constraint.Height,
- maxLines: MaxLines,
- lineHeight: LineHeight);
- }
+ var rects = TextLayout.HitTestTextRange(start, length);
- protected override void OnLostFocus(RoutedEventArgs e)
- {
- base.OnLostFocus(e);
+ using (context.PushPostTransform(Matrix.CreateTranslation(origin)))
+ {
+ foreach (var rect in rects)
+ {
+ context.FillRectangle(selectionBrush, PixelRect.FromRect(rect, 1).ToRect(1));
+ }
+ }
+ }
- ClearSelection();
+ base.RenderTextLayout(context, origin);
}
protected override void OnKeyDown(KeyEventArgs e)
@@ -352,11 +240,6 @@ namespace Avalonia.Controls
{
base.OnPointerPressed(e);
- if (!IsTextSelectionEnabled)
- {
- return;
- }
-
var text = Text;
var clickInfo = e.GetCurrentPoint(this);
@@ -435,11 +318,6 @@ namespace Avalonia.Controls
{
base.OnPointerMoved(e);
- if (!IsTextSelectionEnabled)
- {
- return;
- }
-
// selection should not change during pointer move if the user right clicks
if (e.Pointer.Captured == this && e.GetCurrentPoint(this).Properties.IsLeftButtonPressed)
{
@@ -486,11 +364,6 @@ namespace Avalonia.Controls
{
base.OnPointerReleased(e);
- if (!IsTextSelectionEnabled)
- {
- return;
- }
-
if (e.Pointer.Captured != this)
{
return;
@@ -521,100 +394,15 @@ namespace Avalonia.Controls
e.Pointer.Capture(null);
}
- protected override void OnPropertyChanged(AvaloniaPropertyChangedEventArgs change)
- {
- base.OnPropertyChanged(change);
-
- switch (change.Property.Name)
- {
- case nameof(Inlines):
- {
- OnInlinesChanged(change.OldValue as InlineCollection, change.NewValue as InlineCollection);
- InvalidateTextLayout();
- break;
- }
- }
- }
-
- protected override Size MeasureOverride(Size availableSize)
+ private void UpdateCommandStates()
{
- if(_textRuns != null)
- {
- LogicalChildren.Clear();
-
- VisualChildren.Clear();
-
- _textRuns = null;
- }
-
- if (Inlines != null && Inlines.Count > 0)
- {
- var inlines = Inlines;
-
- var textRuns = new List();
-
- foreach (var inline in inlines)
- {
- inline.BuildTextRun(textRuns);
- }
-
- foreach (var textRun in textRuns)
- {
- if (textRun is EmbeddedControlRun controlRun &&
- controlRun.Control is Control control)
- {
- LogicalChildren.Add(control);
-
- VisualChildren.Add(control);
-
- control.Measure(Size.Infinity);
- }
- }
-
- _textRuns = textRuns;
- }
-
- return base.MeasureOverride(availableSize);
- }
-
- protected override Size ArrangeOverride(Size finalSize)
- {
- if (HasComplexContent)
- {
- var currentY = 0.0;
-
- foreach (var textLine in TextLayout.TextLines)
- {
- var currentX = textLine.Start;
-
- foreach (var run in textLine.TextRuns)
- {
- if (run is DrawableTextRun drawable)
- {
- if (drawable is EmbeddedControlRun controlRun
- && controlRun.Control is Control control)
- {
- control.Arrange(new Rect(new Point(currentX, currentY), control.DesiredSize));
- }
-
- currentX += drawable.Size.Width;
- }
- }
+ var text = GetSelection();
- currentY += textLine.Height;
- }
- }
-
- return base.ArrangeOverride(finalSize);
+ CanCopy = !string.IsNullOrEmpty(text);
}
private string GetSelection()
{
- if (!IsTextSelectionEnabled)
- {
- return "";
- }
-
var text = GetText();
if (string.IsNullOrEmpty(text))
@@ -638,59 +426,5 @@ namespace Avalonia.Controls
return selectedText;
}
-
- private void OnInlinesChanged(InlineCollection? oldValue, InlineCollection? newValue)
- {
- if (oldValue is not null)
- {
- oldValue.Parent = null;
- oldValue.InlineHost = null;
- oldValue.Invalidated -= (s, e) => InvalidateTextLayout();
- }
-
- if (newValue is not null)
- {
- newValue.Parent = this;
- newValue.InlineHost = this;
- newValue.Invalidated += (s, e) => InvalidateTextLayout();
- }
- }
-
- void IInlineHost.Invalidate()
- {
- InvalidateTextLayout();
- }
-
- private readonly struct InlinesTextSource : ITextSource
- {
- private readonly IReadOnlyList _textRuns;
-
- public InlinesTextSource(IReadOnlyList textRuns)
- {
- _textRuns = textRuns;
- }
-
- public TextRun? GetTextRun(int textSourceIndex)
- {
- var currentPosition = 0;
-
- foreach (var textRun in _textRuns)
- {
- if (textRun.TextSourceLength == 0)
- {
- continue;
- }
-
- if (currentPosition >= textSourceIndex)
- {
- return textRun;
- }
-
- currentPosition += textRun.TextSourceLength;
- }
-
- return null;
- }
- }
}
}
diff --git a/src/Avalonia.Controls/SizeChangedEventArgs.cs b/src/Avalonia.Controls/SizeChangedEventArgs.cs
new file mode 100644
index 0000000000..2dc642b163
--- /dev/null
+++ b/src/Avalonia.Controls/SizeChangedEventArgs.cs
@@ -0,0 +1,83 @@
+using Avalonia.Interactivity;
+using Avalonia.Layout;
+using Avalonia.Utilities;
+
+namespace Avalonia.Controls
+{
+ ///
+ /// Provides data specific to a SizeChanged event.
+ ///
+ public class SizeChangedEventArgs : RoutedEventArgs
+ {
+ ///
+ /// Initializes a new instance of the class.
+ ///
+ /// The routed event associated with these event args.
+ public SizeChangedEventArgs(RoutedEvent? routedEvent)
+ : base (routedEvent)
+ {
+ }
+
+ ///
+ /// Initializes a new instance of the class.
+ ///
+ /// The routed event associated with these event args.
+ /// The source object that raised the routed event.
+ public SizeChangedEventArgs(RoutedEvent? routedEvent, IInteractive? source)
+ : base(routedEvent, source)
+ {
+ }
+
+ ///
+ /// Initializes a new instance of the class.
+ ///
+ /// The routed event associated with these event args.
+ /// The source object that raised the routed event.
+ /// The previous size (or bounds) of the object.
+ /// The new size (or bounds) of the object.
+ public SizeChangedEventArgs(
+ RoutedEvent? routedEvent,
+ IInteractive? source,
+ Size previousSize,
+ Size newSize)
+ : base(routedEvent, source)
+ {
+ PreviousSize = previousSize;
+ NewSize = newSize;
+ }
+
+ ///
+ /// Gets a value indicating whether the height of the new size is considered
+ /// different than the previous size height.
+ ///
+ ///
+ /// This will take into account layout epsilon and will not be true if both
+ /// heights are considered equivalent for layout purposes. Remember there can
+ /// be small variations in the calculations between layout cycles due to
+ /// rounding and precision even when the size has not otherwise changed.
+ ///
+ public bool HeightChanged => !MathUtilities.AreClose(NewSize.Height, PreviousSize.Height, LayoutHelper.LayoutEpsilon);
+
+ ///
+ /// Gets the new size (or bounds) of the object.
+ ///
+ public Size NewSize { get; init; }
+
+ ///
+ /// Gets the previous size (or bounds) of the object.
+ ///
+ public Size PreviousSize { get; init; }
+
+ ///
+ /// Gets a value indicating whether the width of the new size is considered
+ /// different than the previous size width.
+ ///
+ ///
+ /// This will take into account layout epsilon and will not be true if both
+ /// widths are considered equivalent for layout purposes. Remember there can
+ /// be small variations in the calculations between layout cycles due to
+ /// rounding and precision even when the size has not otherwise changed.
+ ///
+ public bool WidthChanged => !MathUtilities.AreClose(NewSize.Width, PreviousSize.Width, LayoutHelper.LayoutEpsilon);
+ }
+}
diff --git a/src/Avalonia.Controls/TextBlock.cs b/src/Avalonia.Controls/TextBlock.cs
index 99c8068b3d..0492c2c1e3 100644
--- a/src/Avalonia.Controls/TextBlock.cs
+++ b/src/Avalonia.Controls/TextBlock.cs
@@ -1,4 +1,5 @@
using System;
+using System.Collections.Generic;
using Avalonia.Automation.Peers;
using Avalonia.Controls.Documents;
using Avalonia.Layout;
@@ -12,7 +13,7 @@ namespace Avalonia.Controls
///
/// A control that displays a block of text.
///
- public class TextBlock : Control, IAddChild
+ public class TextBlock : Control, IInlineHost
{
///
/// Defines the property.
@@ -81,6 +82,15 @@ namespace Avalonia.Controls
validate: IsValidLineHeight,
inherits: true);
+ ///
+ /// Defines the property.
+ ///
+ public static readonly AttachedProperty LetterSpacingProperty =
+ AvaloniaProperty.RegisterAttached(
+ nameof(LetterSpacing),
+ 0,
+ inherits: true);
+
///
/// Defines the property.
///
@@ -96,15 +106,15 @@ namespace Avalonia.Controls
public static readonly DirectProperty TextProperty =
AvaloniaProperty.RegisterDirect(
nameof(Text),
- o => o.Text,
- (o, v) => o.Text = v);
+ o => o.GetText(),
+ (o, v) => o.SetText(v));
///
/// Defines the property.
///
public static readonly AttachedProperty TextAlignmentProperty =
AvaloniaProperty.RegisterAttached(
- nameof(TextAlignment),
+ nameof(TextAlignment),
defaultValue: TextAlignment.Start,
inherits: true);
@@ -112,14 +122,14 @@ namespace Avalonia.Controls
/// Defines the property.
///
public static readonly AttachedProperty TextWrappingProperty =
- AvaloniaProperty.RegisterAttached(nameof(TextWrapping),
+ AvaloniaProperty.RegisterAttached(nameof(TextWrapping),
inherits: true);
///
/// Defines the property.
///
public static readonly AttachedProperty TextTrimmingProperty =
- AvaloniaProperty.RegisterAttached(nameof(TextTrimming),
+ AvaloniaProperty.RegisterAttached(nameof(TextTrimming),
defaultValue: TextTrimming.None,
inherits: true);
@@ -129,9 +139,17 @@ namespace Avalonia.Controls
public static readonly StyledProperty TextDecorationsProperty =
AvaloniaProperty.Register(nameof(TextDecorations));
+ ///
+ /// Defines the property.
+ ///
+ public static readonly StyledProperty InlinesProperty =
+ AvaloniaProperty.Register(
+ nameof(Inlines));
+
internal string? _text;
protected TextLayout? _textLayout;
protected Size _constraint;
+ private IReadOnlyList? _textRuns;
///
/// Initializes static members of the class.
@@ -139,10 +157,19 @@ namespace Avalonia.Controls
static TextBlock()
{
ClipToBoundsProperty.OverrideDefaultValue(true);
-
+
AffectsRender(BackgroundProperty, ForegroundProperty);
}
+ public TextBlock()
+ {
+ Inlines = new InlineCollection
+ {
+ LogicalChildren = LogicalChildren,
+ InlineHost = this
+ };
+ }
+
///
/// Gets the used to render the text.
///
@@ -244,6 +271,15 @@ namespace Avalonia.Controls
set => SetValue(LineHeightProperty, value);
}
+ ///
+ /// Gets or sets the letter spacing.
+ ///
+ public double LetterSpacing
+ {
+ get => GetValue(LetterSpacingProperty);
+ set => SetValue(LetterSpacingProperty, value);
+ }
+
///
/// Gets or sets the maximum number of text lines.
///
@@ -288,9 +324,21 @@ namespace Avalonia.Controls
get => GetValue(TextDecorationsProperty);
set => SetValue(TextDecorationsProperty, value);
}
-
+
+ ///
+ /// Gets or sets the inlines.
+ ///
+ [Content]
+ public InlineCollection? Inlines
+ {
+ get => GetValue(InlinesProperty);
+ set => SetValue(InlinesProperty, value);
+ }
+
protected override bool BypassFlowDirectionPolicies => true;
+ internal bool HasComplexContent => Inlines != null && Inlines.Count > 0;
+
///
/// The BaselineOffset property provides an adjustment to baseline offset
///
@@ -445,6 +493,35 @@ namespace Avalonia.Controls
control.SetValue(LineHeightProperty, height);
}
+ ///
+ /// Reads the attached property from the given element
+ ///
+ /// The element to which to read the attached property.
+ public static double GetLetterSpacing(Control control)
+ {
+ if (control == null)
+ {
+ throw new ArgumentNullException(nameof(control));
+ }
+
+ return control.GetValue(LetterSpacingProperty);
+ }
+
+ ///
+ /// Writes the attached property LetterSpacing to the given element.
+ ///
+ /// The element to which to write the attached property.
+ /// The property value to set
+ public static void SetLetterSpacing(Control control, double letterSpacing)
+ {
+ if (control == null)
+ {
+ throw new ArgumentNullException(nameof(control));
+ }
+
+ control.SetValue(LetterSpacingProperty, letterSpacing);
+ }
+
///
/// Reads the attached property from the given element
///
@@ -513,19 +590,30 @@ namespace Avalonia.Controls
TextLayout.Draw(context, origin);
}
- void IAddChild.AddChild(string text)
- {
- _text = text;
- }
-
protected virtual string? GetText()
{
- return _text;
+ return _text ?? Inlines?.Text;
}
protected virtual void SetText(string? text)
{
- SetAndRaise(TextProperty, ref _text, text);
+ if (Inlines != null && Inlines.Count > 0)
+ {
+ var oldValue = Inlines.Text;
+
+ if (!string.IsNullOrEmpty(text))
+ {
+ Inlines.Add(text);
+ }
+
+ text = Inlines.Text;
+
+ RaisePropertyChanged(TextProperty, oldValue, text);
+ }
+ else
+ {
+ SetAndRaise(TextProperty, ref _text, text);
+ }
}
///
@@ -534,17 +622,30 @@ namespace Avalonia.Controls
/// A object.
protected virtual TextLayout CreateTextLayout(string? text)
{
+ var typeface = new Typeface(FontFamily, FontStyle, FontWeight, FontStretch);
+
var defaultProperties = new GenericTextRunProperties(
- new Typeface(FontFamily, FontStyle, FontWeight, FontStretch),
+ typeface,
FontSize,
TextDecorations,
Foreground);
var paragraphProperties = new GenericTextParagraphProperties(FlowDirection, TextAlignment, true, false,
- defaultProperties, TextWrapping, LineHeight, 0);
+ defaultProperties, TextWrapping, LineHeight, 0, LetterSpacing);
+
+ ITextSource textSource;
+
+ if (_textRuns != null)
+ {
+ textSource = new InlinesTextSource(_textRuns);
+ }
+ else
+ {
+ textSource = new SimpleTextSource((text ?? "").AsMemory(), defaultProperties);
+ }
return new TextLayout(
- new SimpleTextSource((text ?? "").AsMemory(), defaultProperties),
+ textSource,
paragraphProperties,
TextTrimming,
_constraint.Width,
@@ -560,6 +661,8 @@ namespace Avalonia.Controls
{
_textLayout = null;
+ InvalidateVisual();
+
InvalidateMeasure();
}
@@ -573,7 +676,46 @@ namespace Avalonia.Controls
_textLayout = null;
- InvalidateArrange();
+ var inlines = Inlines;
+
+ if (HasComplexContent)
+ {
+ if (_textRuns != null)
+ {
+ foreach (var textRun in _textRuns)
+ {
+ if (textRun is EmbeddedControlRun controlRun &&
+ controlRun.Control is Control control)
+ {
+ VisualChildren.Remove(control);
+
+ LogicalChildren.Remove(control);
+ }
+ }
+ }
+
+ var textRuns = new List();
+
+ foreach (var inline in inlines!)
+ {
+ inline.BuildTextRun(textRuns);
+ }
+
+ foreach (var textRun in textRuns)
+ {
+ if (textRun is EmbeddedControlRun controlRun &&
+ controlRun.Control is Control control)
+ {
+ VisualChildren.Add(control);
+
+ LogicalChildren.Add(control);
+
+ control.Measure(Size.Infinity);
+ }
+ }
+
+ _textRuns = textRuns;
+ }
var measuredSize = TextLayout.Bounds.Size.Inflate(padding);
@@ -584,16 +726,11 @@ namespace Avalonia.Controls
{
var textWidth = Math.Ceiling(TextLayout.Bounds.Width);
- if(finalSize.Width < textWidth)
+ if (finalSize.Width < textWidth)
{
finalSize = finalSize.WithWidth(textWidth);
}
- if (MathUtilities.AreClose(_constraint.Width, finalSize.Width))
- {
- return finalSize;
- }
-
var scale = LayoutHelper.GetLayoutScale(this);
var padding = LayoutHelper.RoundLayoutThickness(Padding, scale, scale);
@@ -602,6 +739,32 @@ namespace Avalonia.Controls
_textLayout = null;
+ if (HasComplexContent)
+ {
+ var currentY = padding.Top;
+
+ foreach (var textLine in TextLayout.TextLines)
+ {
+ var currentX = padding.Left + textLine.Start;
+
+ foreach (var run in textLine.TextRuns)
+ {
+ if (run is DrawableTextRun drawable)
+ {
+ if (drawable is EmbeddedControlRun controlRun
+ && controlRun.Control is Control control)
+ {
+ control.Arrange(new Rect(new Point(currentX, currentY), control.DesiredSize));
+ }
+
+ currentX += drawable.Size.Width;
+ }
+ }
+
+ currentY += textLine.Height;
+ }
+ }
+
return finalSize;
}
@@ -610,42 +773,71 @@ namespace Avalonia.Controls
return new TextBlockAutomationPeer(this);
}
- private static bool IsValidMaxLines(int maxLines) => maxLines >= 0;
-
- private static bool IsValidLineHeight(double lineHeight) => double.IsNaN(lineHeight) || lineHeight > 0;
-
protected override void OnPropertyChanged(AvaloniaPropertyChangedEventArgs change)
{
base.OnPropertyChanged(change);
switch (change.Property.Name)
{
- case nameof (FontSize):
- case nameof (FontWeight):
- case nameof (FontStyle):
- case nameof (FontFamily):
- case nameof (FontStretch):
+ case nameof(FontSize):
+ case nameof(FontWeight):
+ case nameof(FontStyle):
+ case nameof(FontFamily):
+ case nameof(FontStretch):
- case nameof (TextWrapping):
- case nameof (TextTrimming):
- case nameof (TextAlignment):
+ case nameof(TextWrapping):
+ case nameof(TextTrimming):
+ case nameof(TextAlignment):
- case nameof (FlowDirection):
+ case nameof(FlowDirection):
case nameof (Padding):
case nameof (LineHeight):
+ case nameof (LetterSpacing):
case nameof (MaxLines):
- case nameof (Text):
- case nameof (TextDecorations):
- case nameof (Foreground):
- {
- InvalidateTextLayout();
- break;
- }
+ case nameof(Text):
+ case nameof(TextDecorations):
+ case nameof(Foreground):
+ {
+ InvalidateTextLayout();
+ break;
+ }
+ case nameof(Inlines):
+ {
+ OnInlinesChanged(change.OldValue as InlineCollection, change.NewValue as InlineCollection);
+ InvalidateTextLayout();
+ break;
+ }
}
}
+ private static bool IsValidMaxLines(int maxLines) => maxLines >= 0;
+
+ private static bool IsValidLineHeight(double lineHeight) => double.IsNaN(lineHeight) || lineHeight > 0;
+
+ private void OnInlinesChanged(InlineCollection? oldValue, InlineCollection? newValue)
+ {
+ if (oldValue is not null)
+ {
+ oldValue.LogicalChildren = null;
+ oldValue.InlineHost = null;
+ oldValue.Invalidated -= (s, e) => InvalidateTextLayout();
+ }
+
+ if (newValue is not null)
+ {
+ newValue.LogicalChildren = LogicalChildren;
+ newValue.InlineHost = this;
+ newValue.Invalidated += (s, e) => InvalidateTextLayout();
+ }
+ }
+
+ void IInlineHost.Invalidate()
+ {
+ InvalidateTextLayout();
+ }
+
protected readonly struct SimpleTextSource : ITextSource
{
private readonly ReadOnlySlice _text;
@@ -674,5 +866,46 @@ namespace Avalonia.Controls
return new TextCharacters(runText, _defaultProperties);
}
}
+
+ private readonly struct InlinesTextSource : ITextSource
+ {
+ private readonly IReadOnlyList _textRuns;
+
+ public InlinesTextSource(IReadOnlyList textRuns)
+ {
+ _textRuns = textRuns;
+ }
+
+ public IReadOnlyList TextRuns => _textRuns;
+
+ public TextRun? GetTextRun(int textSourceIndex)
+ {
+ var currentPosition = 0;
+
+ foreach (var textRun in _textRuns)
+ {
+ if (textRun.TextSourceLength == 0)
+ {
+ continue;
+ }
+
+ if (textSourceIndex >= currentPosition + textRun.TextSourceLength)
+ {
+ currentPosition += textRun.TextSourceLength;
+
+ continue;
+ }
+
+ if (textRun is TextCharacters)
+ {
+ return new TextCharacters(textRun.Text.Skip(Math.Max(0, textSourceIndex - currentPosition)), textRun.Properties!);
+ }
+
+ return textRun;
+ }
+
+ return null;
+ }
+ }
}
}
diff --git a/src/Avalonia.Controls/TextBox.cs b/src/Avalonia.Controls/TextBox.cs
index 331c75cd1f..93dad5b367 100644
--- a/src/Avalonia.Controls/TextBox.cs
+++ b/src/Avalonia.Controls/TextBox.cs
@@ -113,6 +113,12 @@ namespace Avalonia.Controls
public static readonly StyledProperty LineHeightProperty =
TextBlock.LineHeightProperty.AddOwner();
+ ///
+ /// Defines see property.
+ ///
+ public static readonly StyledProperty LetterSpacingProperty =
+ TextBlock.LetterSpacingProperty.AddOwner();
+
public static readonly StyledProperty WatermarkProperty =
AvaloniaProperty.Register(nameof(Watermark));
@@ -377,6 +383,12 @@ namespace Avalonia.Controls
set => SetValue(MaxLinesProperty, value);
}
+ public double LetterSpacing
+ {
+ get => GetValue(LetterSpacingProperty);
+ set => SetValue(LetterSpacingProperty, value);
+ }
+
///
/// Gets or sets the line height.
///
diff --git a/src/Avalonia.Controls/TreeViewItem.cs b/src/Avalonia.Controls/TreeViewItem.cs
index ada081b808..2f96e6911f 100644
--- a/src/Avalonia.Controls/TreeViewItem.cs
+++ b/src/Avalonia.Controls/TreeViewItem.cs
@@ -6,6 +6,7 @@ using Avalonia.Controls.Primitives;
using Avalonia.Controls.Templates;
using Avalonia.Input;
using Avalonia.LogicalTree;
+using Avalonia.Threading;
namespace Avalonia.Controls
{
@@ -45,6 +46,8 @@ namespace Avalonia.Controls
private IControl? _header;
private bool _isExpanded;
private int _level;
+ private bool _templateApplied;
+ private bool _deferredBringIntoViewFlag;
///
/// Initializes static members of the class.
@@ -136,15 +139,24 @@ namespace Avalonia.Controls
protected virtual void OnRequestBringIntoView(RequestBringIntoViewEventArgs e)
{
- if (e.TargetObject == this && _header != null)
+ if (e.TargetObject == this)
{
- var m = _header.TransformToVisual(this);
+ if (!_templateApplied)
+ {
+ _deferredBringIntoViewFlag = true;
+ return;
+ }
- if (m.HasValue)
+ if (_header != null)
{
- var bounds = new Rect(_header.Bounds.Size);
- var rect = bounds.TransformToAABB(m.Value);
- e.TargetRect = rect;
+ var m = _header.TransformToVisual(this);
+
+ if (m.HasValue)
+ {
+ var bounds = new Rect(_header.Bounds.Size);
+ var rect = bounds.TransformToAABB(m.Value);
+ e.TargetRect = rect;
+ }
}
}
}
@@ -187,6 +199,12 @@ namespace Avalonia.Controls
protected override void OnApplyTemplate(TemplateAppliedEventArgs e)
{
_header = e.NameScope.Find("PART_Header");
+ _templateApplied = true;
+ if (_deferredBringIntoViewFlag)
+ {
+ _deferredBringIntoViewFlag = false;
+ Dispatcher.UIThread.Post(this.BringIntoView); // must use the Dispatcher, otherwise the TreeView doesn't scroll
+ }
}
private static int CalculateDistanceFromLogicalParent(ILogical? logical, int @default = -1) where T : class
diff --git a/src/Avalonia.Headless/HeadlessPlatformRenderInterface.cs b/src/Avalonia.Headless/HeadlessPlatformRenderInterface.cs
index fcd7f1e31f..7abc0ca131 100644
--- a/src/Avalonia.Headless/HeadlessPlatformRenderInterface.cs
+++ b/src/Avalonia.Headless/HeadlessPlatformRenderInterface.cs
@@ -115,19 +115,16 @@ namespace Avalonia.Headless
return new HeadlessGeometryStub(new Rect(glyphRun.Size));
}
- public IGlyphRunBuffer AllocateGlyphRun(IGlyphTypeface glyphTypeface, float fontRenderingEmSize, int length)
+ public IGlyphRunImpl CreateGlyphRun(IGlyphTypeface glyphTypeface, double fontRenderingEmSize, IReadOnlyList glyphIndices, IReadOnlyList glyphAdvances, IReadOnlyList glyphOffsets)
{
- return new HeadlessGlyphRunBufferStub();
+ return new HeadlessGlyphRunStub();
}
- public IHorizontalGlyphRunBuffer AllocateHorizontalGlyphRun(IGlyphTypeface glyphTypeface, float fontRenderingEmSize, int length)
- {
- return new HeadlessHorizontalGlyphRunBufferStub();
- }
-
- public IPositionedGlyphRunBuffer AllocatePositionedGlyphRun(IGlyphTypeface glyphTypeface, float fontRenderingEmSize, int length)
+ class HeadlessGlyphRunStub : IGlyphRunImpl
{
- return new HeadlessPositionedGlyphRunBufferStub();
+ public void Dispose()
+ {
+ }
}
class HeadlessGeometryStub : IGeometryImpl
@@ -213,33 +210,6 @@ namespace Avalonia.Headless
public Matrix Transform { get; }
}
- class HeadlessGlyphRunBufferStub : IGlyphRunBuffer
- {
- public Span GlyphIndices => Span.Empty;
-
- public IGlyphRunImpl Build()
- {
- return new HeadlessGlyphRunStub();
- }
- }
-
- class HeadlessHorizontalGlyphRunBufferStub : HeadlessGlyphRunBufferStub, IHorizontalGlyphRunBuffer
- {
- public Span GlyphPositions => Span.Empty;
- }
-
- class HeadlessPositionedGlyphRunBufferStub : HeadlessGlyphRunBufferStub, IPositionedGlyphRunBuffer
- {
- public Span GlyphPositions => Span.Empty;
- }
-
- class HeadlessGlyphRunStub : IGlyphRunImpl
- {
- public void Dispose()
- {
- }
- }
-
class HeadlessStreamingGeometryStub : HeadlessGeometryStub, IStreamGeometryImpl
{
public HeadlessStreamingGeometryStub() : base(Rect.Empty)
@@ -353,12 +323,12 @@ namespace Avalonia.Headless
public Vector Dpi { get; }
public PixelSize PixelSize { get; }
public int Version { get; set; }
- public void Save(string fileName)
+ public void Save(string fileName, int? quality = null)
{
}
- public void Save(Stream stream)
+ public void Save(Stream stream, int? quality = null)
{
}
diff --git a/src/Avalonia.Headless/HeadlessPlatformStubs.cs b/src/Avalonia.Headless/HeadlessPlatformStubs.cs
index c8ac947c16..76948e9286 100644
--- a/src/Avalonia.Headless/HeadlessPlatformStubs.cs
+++ b/src/Avalonia.Headless/HeadlessPlatformStubs.cs
@@ -102,6 +102,8 @@ namespace Avalonia.Headless
public int GlyphCount => 1337;
+ public FontSimulations FontSimulations { get; }
+
public void Dispose()
{
}
@@ -138,6 +140,17 @@ namespace Avalonia.Headless
table = null;
return false;
}
+
+ public bool TryGetGlyphMetrics(ushort glyph, out GlyphMetrics metrics)
+ {
+ metrics = new GlyphMetrics
+ {
+ Height = 10,
+ Width = 10
+ };
+
+ return true;
+ }
}
class HeadlessTextShaperStub : ITextShaperImpl
diff --git a/src/Avalonia.MicroCom/Avalonia.MicroCom.csproj b/src/Avalonia.MicroCom/Avalonia.MicroCom.csproj
index e784bda105..14dafb7284 100644
--- a/src/Avalonia.MicroCom/Avalonia.MicroCom.csproj
+++ b/src/Avalonia.MicroCom/Avalonia.MicroCom.csproj
@@ -4,7 +4,7 @@
true
-
+
false
all
diff --git a/src/Avalonia.Native/Avalonia.Native.csproj b/src/Avalonia.Native/Avalonia.Native.csproj
index 4ceb1be340..c924ef3a45 100644
--- a/src/Avalonia.Native/Avalonia.Native.csproj
+++ b/src/Avalonia.Native/Avalonia.Native.csproj
@@ -24,7 +24,7 @@
-
+
diff --git a/src/Avalonia.Native/AvaloniaNativePlatform.cs b/src/Avalonia.Native/AvaloniaNativePlatform.cs
index 532893884a..66d78cbb60 100644
--- a/src/Avalonia.Native/AvaloniaNativePlatform.cs
+++ b/src/Avalonia.Native/AvaloniaNativePlatform.cs
@@ -103,6 +103,8 @@ namespace Avalonia.Native
var macOpts = AvaloniaLocator.Current.GetService() ?? new MacOSPlatformOptions();
_factory.MacOptions.SetShowInDock(macOpts.ShowInDock ? 1 : 0);
+ _factory.MacOptions.SetDisableSetProcessName(macOpts.DisableSetProcessName ? 1 : 0);
+ _factory.MacOptions.SetDisableAppDelegate(macOpts.DisableAvaloniaAppDelegate ? 1 : 0);
}
AvaloniaLocator.CurrentMutable
diff --git a/src/Avalonia.Native/AvaloniaNativePlatformExtensions.cs b/src/Avalonia.Native/AvaloniaNativePlatformExtensions.cs
index 61889aa9e4..189f45d7c8 100644
--- a/src/Avalonia.Native/AvaloniaNativePlatformExtensions.cs
+++ b/src/Avalonia.Native/AvaloniaNativePlatformExtensions.cs
@@ -83,5 +83,13 @@ namespace Avalonia
/// Gets or sets a value indicating whether the native macOS menu bar will be enabled for the application.
///
public bool DisableNativeMenus { get; set; }
+
+ public bool DisableSetProcessName { get; set; }
+
+ ///
+ /// Gets or sets a value indicating whether Avalonia can install its own AppDelegate.
+ /// Disabling this can be useful in some scenarios like when running as a plugin inside an existing macOS application.
+ ///
+ public bool DisableAvaloniaAppDelegate { get; set; }
}
}
diff --git a/src/Avalonia.Native/avn.idl b/src/Avalonia.Native/avn.idl
index b0bff07146..b90e144eab 100644
--- a/src/Avalonia.Native/avn.idl
+++ b/src/Avalonia.Native/avn.idl
@@ -599,6 +599,8 @@ interface IAvnMacOptions : IUnknown
{
HRESULT SetShowInDock(int show);
HRESULT SetApplicationTitle(char* utf8string);
+ HRESULT SetDisableSetProcessName(int disable);
+ HRESULT SetDisableAppDelegate(int disable);
}
[uuid(04c1b049-1f43-418a-9159-cae627ec1367)]
diff --git a/src/Avalonia.Themes.Fluent/Accents/FluentControlResourcesDark.xaml b/src/Avalonia.Themes.Fluent/Accents/FluentControlResourcesDark.xaml
index e480fb1670..7e3c8673f5 100644
--- a/src/Avalonia.Themes.Fluent/Accents/FluentControlResourcesDark.xaml
+++ b/src/Avalonia.Themes.Fluent/Accents/FluentControlResourcesDark.xaml
@@ -281,7 +281,6 @@
-
@@ -305,7 +304,39 @@
-
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/src/Avalonia.Themes.Fluent/Accents/FluentControlResourcesLight.xaml b/src/Avalonia.Themes.Fluent/Accents/FluentControlResourcesLight.xaml
index 4f421f64a7..7917315e19 100644
--- a/src/Avalonia.Themes.Fluent/Accents/FluentControlResourcesLight.xaml
+++ b/src/Avalonia.Themes.Fluent/Accents/FluentControlResourcesLight.xaml
@@ -278,7 +278,7 @@
-
+
@@ -301,7 +301,39 @@
-
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/src/Avalonia.Themes.Fluent/Controls/Expander.xaml b/src/Avalonia.Themes.Fluent/Controls/Expander.xaml
index 7d5bb48333..df9de331ee 100644
--- a/src/Avalonia.Themes.Fluent/Controls/Expander.xaml
+++ b/src/Avalonia.Themes.Fluent/Controls/Expander.xaml
@@ -1,6 +1,7 @@
+
@@ -44,45 +45,59 @@
- 16
+
+ 48
+
+
+ Stretch
+ Center
+ 16,0,0,0
+ 1
+ 0
+ 20,0,8,0
+ 32
+
+
16
- 1
- 1,1,0,1
- 1,1,1,0
- 0,1,1,1
- 1,0,1,1
-
-
-
-
-
-
+ 1,1,0,1
+ 1,1,1,0
+ 0,1,1,1
+ 1,0,1,1
+
+
+
+
+
+
+
+
-
+
+ HorizontalContentAlignment="{TemplateBinding HorizontalContentAlignment}"
+ VerticalContentAlignment="{TemplateBinding VerticalContentAlignment}"
+ Foreground="{TemplateBinding Foreground}"
+ Margin="{TemplateBinding Padding}"/>
+ Width="{DynamicResource ExpanderChevronButtonSize}"
+ Height="{DynamicResource ExpanderChevronButtonSize}"
+ Margin="{DynamicResource ExpanderChevronMargin}"
+ CornerRadius="{DynamicResource ControlCornerRadius}"
+ BorderBrush="{DynamicResource ExpanderChevronBorderBrush}"
+ BorderThickness="{DynamicResource ExpanderChevronBorderThickness}"
+ Background="{DynamicResource ExpanderChevronBackground}">
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
-
-
-
+
+
+
+
+
+
+
+
+
-
-
-
-
-
+
+ IsChecked="{TemplateBinding IsExpanded, Mode=TwoWay}" />
+ IsVisible="{TemplateBinding IsExpanded, Mode=TwoWay}"
+ Background="{TemplateBinding Background}"
+ BorderBrush="{TemplateBinding BorderBrush}"
+ BorderThickness="{TemplateBinding BorderThickness}"
+ MinHeight="{TemplateBinding MinHeight}"
+ HorizontalAlignment="Stretch"
+ VerticalAlignment="Stretch"
+ Padding="{TemplateBinding Padding}">
+ ContentTemplate="{TemplateBinding ContentTemplate}"
+ Foreground="{TemplateBinding Foreground}"
+ HorizontalContentAlignment="{TemplateBinding HorizontalContentAlignment}"
+ VerticalContentAlignment="{TemplateBinding VerticalContentAlignment}" />
@@ -241,16 +316,16 @@
diff --git a/src/Avalonia.Themes.Fluent/Controls/FluentControls.xaml b/src/Avalonia.Themes.Fluent/Controls/FluentControls.xaml
index 577539b26b..5383aa3180 100644
--- a/src/Avalonia.Themes.Fluent/Controls/FluentControls.xaml
+++ b/src/Avalonia.Themes.Fluent/Controls/FluentControls.xaml
@@ -68,7 +68,7 @@
-
+
diff --git a/src/Avalonia.Themes.Fluent/Controls/RichTextBlock.xaml b/src/Avalonia.Themes.Fluent/Controls/RichTextBlock.xaml
deleted file mode 100644
index 75af2efcb1..0000000000
--- a/src/Avalonia.Themes.Fluent/Controls/RichTextBlock.xaml
+++ /dev/null
@@ -1,14 +0,0 @@
-
-
-
-
-
-
-
-
-
-
-
diff --git a/src/Avalonia.Themes.Fluent/Controls/SelectableTextBlock.xaml b/src/Avalonia.Themes.Fluent/Controls/SelectableTextBlock.xaml
new file mode 100644
index 0000000000..f630969ae6
--- /dev/null
+++ b/src/Avalonia.Themes.Fluent/Controls/SelectableTextBlock.xaml
@@ -0,0 +1,18 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/src/Avalonia.Themes.Fluent/Controls/TextBox.xaml b/src/Avalonia.Themes.Fluent/Controls/TextBox.xaml
index 17c69da8fd..db487ef76b 100644
--- a/src/Avalonia.Themes.Fluent/Controls/TextBox.xaml
+++ b/src/Avalonia.Themes.Fluent/Controls/TextBox.xaml
@@ -161,6 +161,7 @@
TextAlignment="{TemplateBinding TextAlignment}"
TextWrapping="{TemplateBinding TextWrapping}"
LineHeight="{TemplateBinding LineHeight}"
+ LetterSpacing="{TemplateBinding LetterSpacing}"
PasswordChar="{TemplateBinding PasswordChar}"
RevealPassword="{TemplateBinding RevealPassword}"
SelectionBrush="{TemplateBinding SelectionBrush}"
diff --git a/src/Avalonia.Themes.Simple/Controls/RichTextBlock.xaml b/src/Avalonia.Themes.Simple/Controls/RichTextBlock.xaml
deleted file mode 100644
index c0570282cb..0000000000
--- a/src/Avalonia.Themes.Simple/Controls/RichTextBlock.xaml
+++ /dev/null
@@ -1,14 +0,0 @@
-
-
-
-
-
-
-
-
-
diff --git a/src/Avalonia.Themes.Simple/Controls/SelectableTextBlock.xaml b/src/Avalonia.Themes.Simple/Controls/SelectableTextBlock.xaml
new file mode 100644
index 0000000000..aaa6448aea
--- /dev/null
+++ b/src/Avalonia.Themes.Simple/Controls/SelectableTextBlock.xaml
@@ -0,0 +1,18 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/src/Avalonia.Themes.Simple/Controls/SimpleControls.xaml b/src/Avalonia.Themes.Simple/Controls/SimpleControls.xaml
index 644c6ed416..4aefa0136c 100644
--- a/src/Avalonia.Themes.Simple/Controls/SimpleControls.xaml
+++ b/src/Avalonia.Themes.Simple/Controls/SimpleControls.xaml
@@ -64,7 +64,7 @@
-
+
diff --git a/src/Avalonia.Themes.Simple/Controls/TextBox.xaml b/src/Avalonia.Themes.Simple/Controls/TextBox.xaml
index 5fa6412688..0bcb425ca9 100644
--- a/src/Avalonia.Themes.Simple/Controls/TextBox.xaml
+++ b/src/Avalonia.Themes.Simple/Controls/TextBox.xaml
@@ -149,14 +149,14 @@
CaretBrush="{TemplateBinding CaretBrush}"
CaretIndex="{TemplateBinding CaretIndex}"
LineHeight="{TemplateBinding LineHeight}"
+ LetterSpacing="{TemplateBinding LetterSpacing}"
PasswordChar="{TemplateBinding PasswordChar}"
RevealPassword="{TemplateBinding RevealPassword}"
SelectionBrush="{TemplateBinding SelectionBrush}"
SelectionEnd="{TemplateBinding SelectionEnd}"
SelectionForegroundBrush="{TemplateBinding SelectionForegroundBrush}"
SelectionStart="{TemplateBinding SelectionStart}"
- Text="{TemplateBinding Text,
- Mode=TwoWay}"
+ Text="{TemplateBinding Text,Mode=TwoWay}"
TextAlignment="{TemplateBinding TextAlignment}"
TextWrapping="{TemplateBinding TextWrapping}" />
diff --git a/src/Markup/Avalonia.Markup.Xaml.Loader/CompilerExtensions/AvaloniaXamlIlCompiler.cs b/src/Markup/Avalonia.Markup.Xaml.Loader/CompilerExtensions/AvaloniaXamlIlCompiler.cs
index f325e6e2d6..4ece433530 100644
--- a/src/Markup/Avalonia.Markup.Xaml.Loader/CompilerExtensions/AvaloniaXamlIlCompiler.cs
+++ b/src/Markup/Avalonia.Markup.Xaml.Loader/CompilerExtensions/AvaloniaXamlIlCompiler.cs
@@ -1,5 +1,6 @@
using System.Collections.Generic;
using System.Linq;
+
using Avalonia.Markup.Xaml.XamlIl.CompilerExtensions.Transformers;
using XamlX;
using XamlX.Ast;
@@ -51,6 +52,7 @@ namespace Avalonia.Markup.Xaml.XamlIl.CompilerExtensions
new AvaloniaXamlIlControlTemplateTargetTypeMetadataTransformer(),
new AvaloniaXamlIlBindingPathParser(),
new AvaloniaXamlIlPropertyPathTransformer(),
+ new AvaloniaXamlIlSetterTargetTypeMetadataTransformer(),
new AvaloniaXamlIlSetterTransformer(),
new AvaloniaXamlIlConstructorServiceProviderTransformer(),
new AvaloniaXamlIlTransitionsTypeMetadataTransformer(),
diff --git a/src/Markup/Avalonia.Markup.Xaml.Loader/CompilerExtensions/Transformers/AvaloniaXamlIlSetterTargetTypeMetadataTransformer.cs b/src/Markup/Avalonia.Markup.Xaml.Loader/CompilerExtensions/Transformers/AvaloniaXamlIlSetterTargetTypeMetadataTransformer.cs
new file mode 100644
index 0000000000..ebc6c01ba8
--- /dev/null
+++ b/src/Markup/Avalonia.Markup.Xaml.Loader/CompilerExtensions/Transformers/AvaloniaXamlIlSetterTargetTypeMetadataTransformer.cs
@@ -0,0 +1,34 @@
+using System.Linq;
+using XamlX;
+using XamlX.Ast;
+using XamlX.Transform;
+using XamlX.Transform.Transformers;
+
+namespace Avalonia.Markup.Xaml.XamlIl.CompilerExtensions.Transformers;
+
+internal class AvaloniaXamlIlSetterTargetTypeMetadataTransformer : IXamlAstTransformer
+{
+ public IXamlAstNode Transform(AstTransformationContext context, IXamlAstNode node)
+ {
+ if (node is XamlAstObjectNode on
+ && on.Children.FirstOrDefault(c => c is XamlAstXmlDirective
+ {
+ Namespace: XamlNamespaces.Xaml2006,
+ Name: "SetterTargetType"
+ }) is { } typeDirective)
+ {
+ var value = ((XamlAstXmlDirective)typeDirective).Values.Single();
+ var type = value is XamlTypeExtensionNode typeNode ? typeNode.Value
+ : value is XamlAstTextNode tn ? TypeReferenceResolver.ResolveType(context, tn.Text, false, tn, true)
+ : null;
+ on.Children.Remove(typeDirective);
+
+ if (type is null)
+ {
+ throw new XamlParseException("Unable to resolve SetterTargetType type", typeDirective);
+ }
+ return new AvaloniaXamlIlTargetTypeMetadataNode(on, type, AvaloniaXamlIlTargetTypeMetadataNode.ScopeTypes.Style);
+ }
+ return node;
+ }
+}
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 ceaec972f6..5a6fd8246d 100644
--- a/src/Markup/Avalonia.Markup.Xaml.Loader/CompilerExtensions/Transformers/AvaloniaXamlIlSetterTransformer.cs
+++ b/src/Markup/Avalonia.Markup.Xaml.Loader/CompilerExtensions/Transformers/AvaloniaXamlIlSetterTransformer.cs
@@ -1,5 +1,6 @@
using System.Collections.Generic;
using System.Linq;
+using XamlX;
using XamlX.Ast;
using XamlX.Emit;
using XamlX.IL;
@@ -8,7 +9,6 @@ using XamlX.TypeSystem;
namespace Avalonia.Markup.Xaml.XamlIl.CompilerExtensions.Transformers
{
- using XamlParseException = XamlX.XamlParseException;
class AvaloniaXamlIlSetterTransformer : IXamlAstTransformer
{
public IXamlAstNode Transform(AstTransformationContext context, IXamlAstNode node)
@@ -17,10 +17,24 @@ namespace Avalonia.Markup.Xaml.XamlIl.CompilerExtensions.Transformers
&& on.Type.GetClrType().FullName == "Avalonia.Styling.Setter"))
return node;
- var targetTypeNode = context.ParentNodes()
+ IXamlType targetType = null;
+ IXamlLineInfo lineInfo = null;
+
+ var styleParent = context.ParentNodes()
.OfType()
- .FirstOrDefault(x => x.ScopeType == AvaloniaXamlIlTargetTypeMetadataNode.ScopeTypes.Style) ??
- throw new XamlParseException("Can not find parent Style Selector or ControlTemplate TargetType", node);
+ .FirstOrDefault(x => x.ScopeType == AvaloniaXamlIlTargetTypeMetadataNode.ScopeTypes.Style);
+
+ if (styleParent != null)
+ {
+ targetType = styleParent.TargetType.GetClrType()
+ ?? throw new XamlParseException("Can not find parent Style Selector or ControlTemplate TargetType. If setter is not part of the style, you can set x:SetterTargetType directive on its parent.", node);
+ lineInfo = on;
+ }
+
+ if (targetType == null)
+ {
+ throw new XamlParseException("Could not determine target type of Setter", node);
+ }
IXamlType propType = null;
var property = @on.Children.OfType()
@@ -31,9 +45,8 @@ namespace Avalonia.Markup.Xaml.XamlIl.CompilerExtensions.Transformers
if (propertyName == null)
throw new XamlParseException("Setter.Property must be a string", node);
-
var avaloniaPropertyNode = XamlIlAvaloniaPropertyHelper.CreateNode(context, propertyName,
- new XamlAstClrTypeReference(targetTypeNode, targetTypeNode.TargetType.GetClrType(), false), property.Values[0]);
+ new XamlAstClrTypeReference(lineInfo, targetType, false), property.Values[0]);
property.Values = new List {avaloniaPropertyNode};
propType = avaloniaPropertyNode.AvaloniaPropertyType;
}
diff --git a/src/Markup/Avalonia.Markup.Xaml/Avalonia.Markup.Xaml.csproj b/src/Markup/Avalonia.Markup.Xaml/Avalonia.Markup.Xaml.csproj
index d0c39f0289..75746273c2 100644
--- a/src/Markup/Avalonia.Markup.Xaml/Avalonia.Markup.Xaml.csproj
+++ b/src/Markup/Avalonia.Markup.Xaml/Avalonia.Markup.Xaml.csproj
@@ -9,6 +9,7 @@
CS1591
+
@@ -68,4 +69,8 @@
+
+
+
+
diff --git a/src/Markup/Avalonia.Markup/Markup/Parsers/BindingExpressionGrammar.cs b/src/Markup/Avalonia.Markup/Markup/Parsers/BindingExpressionGrammar.cs
index 0a9fbcfacb..21c0d97c74 100644
--- a/src/Markup/Avalonia.Markup/Markup/Parsers/BindingExpressionGrammar.cs
+++ b/src/Markup/Avalonia.Markup/Markup/Parsers/BindingExpressionGrammar.cs
@@ -168,7 +168,11 @@ namespace Avalonia.Markup.Parsers
}
}
- private static State ParseAttachedProperty(scoped ref CharacterReader r, List nodes)
+ private static State ParseAttachedProperty(
+#if NET7SDK
+ scoped
+#endif
+ ref CharacterReader r, List nodes)
{
var (ns, owner) = ParseTypeName(ref r);
@@ -318,7 +322,11 @@ namespace Avalonia.Markup.Parsers
return State.AfterMember;
}
- private static TypeName ParseTypeName(scoped ref CharacterReader r)
+ private static TypeName ParseTypeName(
+#if NET7SDK
+ scoped
+#endif
+ ref CharacterReader r)
{
ReadOnlySpan ns, typeName;
ns = ReadOnlySpan.Empty;
diff --git a/src/Avalonia.Base/Compatibility/StringCompatibilityExtensions.cs b/src/Shared/StringCompatibilityExtensions.cs
similarity index 83%
rename from src/Avalonia.Base/Compatibility/StringCompatibilityExtensions.cs
rename to src/Shared/StringCompatibilityExtensions.cs
index 45e41b44d6..7c894dba97 100644
--- a/src/Avalonia.Base/Compatibility/StringCompatibilityExtensions.cs
+++ b/src/Shared/StringCompatibilityExtensions.cs
@@ -3,7 +3,7 @@
namespace System;
#if !NET6_0_OR_GREATER
-public static class StringCompatibilityExtensions
+internal static class StringCompatibilityExtensions
{
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static bool Contains(this string str, char search) =>
diff --git a/src/Skia/Avalonia.Skia/FontManagerImpl.cs b/src/Skia/Avalonia.Skia/FontManagerImpl.cs
index 6b5e0b3db5..90ff9652d8 100644
--- a/src/Skia/Avalonia.Skia/FontManagerImpl.cs
+++ b/src/Skia/Avalonia.Skia/FontManagerImpl.cs
@@ -148,11 +148,19 @@ namespace Avalonia.Skia
$"Could not create glyph typeface for: {typeface.FontFamily.Name}.");
}
- var isFakeBold = (int)typeface.Weight >= 600 && !skTypeface.IsBold;
+ var fontSimulations = FontSimulations.None;
- var isFakeItalic = typeface.Style == FontStyle.Italic && !skTypeface.IsItalic;
-
- return new GlyphTypefaceImpl(skTypeface, isFakeBold, isFakeItalic);
+ if((int)typeface.Weight >= 600 && !skTypeface.IsBold)
+ {
+ fontSimulations |= FontSimulations.Bold;
+ }
+
+ if(typeface.Style == FontStyle.Italic && !skTypeface.IsItalic)
+ {
+ fontSimulations |= FontSimulations.Oblique;
+ }
+
+ return new GlyphTypefaceImpl(skTypeface, fontSimulations);
}
}
}
diff --git a/src/Skia/Avalonia.Skia/GlyphTypefaceImpl.cs b/src/Skia/Avalonia.Skia/GlyphTypefaceImpl.cs
index 7f3faf251f..71bdc1bd6b 100644
--- a/src/Skia/Avalonia.Skia/GlyphTypefaceImpl.cs
+++ b/src/Skia/Avalonia.Skia/GlyphTypefaceImpl.cs
@@ -12,7 +12,7 @@ namespace Avalonia.Skia
{
private bool _isDisposed;
- public GlyphTypefaceImpl(SKTypeface typeface, bool isFakeBold = false, bool isFakeItalic = false)
+ public GlyphTypefaceImpl(SKTypeface typeface, FontSimulations fontSimulations)
{
Typeface = typeface ?? throw new ArgumentNullException(nameof(typeface));
@@ -52,9 +52,7 @@ namespace Avalonia.Skia
GlyphCount = Typeface.GlyphCount;
- IsFakeBold = isFakeBold;
-
- IsFakeItalic = isFakeItalic;
+ FontSimulations = fontSimulations;
}
public Face Face { get; }
@@ -63,15 +61,33 @@ namespace Avalonia.Skia
public SKTypeface Typeface { get; }
+ public FontSimulations FontSimulations { get; }
+
public int ReplacementCodepoint { get; }
public FontMetrics Metrics { get; }
public int GlyphCount { get; }
- public bool IsFakeBold { get; }
+ public bool TryGetGlyphMetrics(ushort glyph, out GlyphMetrics metrics)
+ {
+ metrics = default;
- public bool IsFakeItalic { get; }
+ if (!Font.TryGetGlyphExtents(glyph, out var extents))
+ {
+ return false;
+ }
+
+ metrics = new GlyphMetrics
+ {
+ XBearing = extents.XBearing,
+ YBearing = extents.YBearing,
+ Width = extents.Width,
+ Height = extents.Height
+ };
+
+ return true;
+ }
///
public ushort GetGlyph(uint codepoint)
diff --git a/src/Skia/Avalonia.Skia/Gpu/OpenGl/OpenGlBitmapImpl.cs b/src/Skia/Avalonia.Skia/Gpu/OpenGl/OpenGlBitmapImpl.cs
index d700d4848e..857433f95f 100644
--- a/src/Skia/Avalonia.Skia/Gpu/OpenGl/OpenGlBitmapImpl.cs
+++ b/src/Skia/Avalonia.Skia/Gpu/OpenGl/OpenGlBitmapImpl.cs
@@ -25,9 +25,9 @@ namespace Avalonia.Skia
public Vector Dpi { get; }
public PixelSize PixelSize { get; }
public int Version { get; private set; }
- public void Save(string fileName) => throw new NotSupportedException();
+ public void Save(string fileName, int? quality = null) => throw new NotSupportedException();
- public void Save(Stream stream) => throw new NotSupportedException();
+ public void Save(Stream stream, int? quality = null) => throw new NotSupportedException();
public void Draw(DrawingContextImpl context, SKRect sourceRect, SKRect destRect, SKPaint paint)
{
diff --git a/src/Skia/Avalonia.Skia/Helpers/ImageSavingHelper.cs b/src/Skia/Avalonia.Skia/Helpers/ImageSavingHelper.cs
index 5fa961cc99..b4dd754822 100644
--- a/src/Skia/Avalonia.Skia/Helpers/ImageSavingHelper.cs
+++ b/src/Skia/Avalonia.Skia/Helpers/ImageSavingHelper.cs
@@ -14,14 +14,19 @@ namespace Avalonia.Skia.Helpers
///
/// Image to save
/// Target file.
- public static void SaveImage(SKImage image, string fileName)
+ ///
+ /// The optional quality for PNG compression.
+ /// The quality value is interpreted from 0 - 100. If quality is null
+ /// the encoder applies the default quality value.
+ ///
+ public static void SaveImage(SKImage image, string fileName, int? quality = null)
{
if (image == null) throw new ArgumentNullException(nameof(image));
if (fileName == null) throw new ArgumentNullException(nameof(fileName));
using (var stream = File.Create(fileName))
{
- SaveImage(image, stream);
+ SaveImage(image, stream, quality);
}
}
@@ -29,16 +34,30 @@ namespace Avalonia.Skia.Helpers
/// Save Skia image to a stream.
///
/// Image to save
- /// Target stream.
- public static void SaveImage(SKImage image, Stream stream)
+ ///
+ /// The optional quality for PNG compression.
+ /// The quality value is interpreted from 0 - 100. If quality is null
+ /// the encoder applies the default quality value.
+ ///
+ public static void SaveImage(SKImage image, Stream stream, int? quality = null)
{
if (image == null) throw new ArgumentNullException(nameof(image));
if (stream == null) throw new ArgumentNullException(nameof(stream));
- using (var data = image.Encode())
+ if (quality == null)
{
- data.SaveTo(stream);
+ using (var data = image.Encode())
+ {
+ data.SaveTo(stream);
+ }
+ }
+ else
+ {
+ using (var data = image.Encode(SKEncodedImageFormat.Png, (int)quality))
+ {
+ data.SaveTo(stream);
+ }
}
}
}
-}
\ No newline at end of file
+}
diff --git a/src/Skia/Avalonia.Skia/ImmutableBitmap.cs b/src/Skia/Avalonia.Skia/ImmutableBitmap.cs
index 6400d67fde..e24d805050 100644
--- a/src/Skia/Avalonia.Skia/ImmutableBitmap.cs
+++ b/src/Skia/Avalonia.Skia/ImmutableBitmap.cs
@@ -139,13 +139,13 @@ namespace Avalonia.Skia
}
///
- public void Save(string fileName)
+ public void Save(string fileName, int? quality = null)
{
ImageSavingHelper.SaveImage(_image, fileName);
}
///
- public void Save(Stream stream)
+ public void Save(Stream stream, int? quality = null)
{
ImageSavingHelper.SaveImage(_image, stream);
}
diff --git a/src/Skia/Avalonia.Skia/PlatformRenderInterface.cs b/src/Skia/Avalonia.Skia/PlatformRenderInterface.cs
index a9696efbd4..dd3badb2d8 100644
--- a/src/Skia/Avalonia.Skia/PlatformRenderInterface.cs
+++ b/src/Skia/Avalonia.Skia/PlatformRenderInterface.cs
@@ -12,8 +12,6 @@ using Avalonia.OpenGL.Imaging;
using Avalonia.Platform;
using Avalonia.Media.Imaging;
using SkiaSharp;
-using System.Runtime.InteropServices;
-using System.Drawing;
namespace Avalonia.Skia
{
@@ -79,7 +77,7 @@ namespace Avalonia.Skia
var skFont = new SKFont(glyphTypeface.Typeface, fontRenderingEmSize)
{
Size = fontRenderingEmSize,
- Edging = SKFontEdging.Antialias,
+ Edging = SKFontEdging.Alias,
Hinting = SKFontHinting.None,
LinearMetrics = true
};
@@ -244,85 +242,91 @@ namespace Avalonia.Skia
"Current GPU acceleration backend does not support OpenGL integration");
}
- public IGlyphRunBuffer AllocateGlyphRun(IGlyphTypeface glyphTypeface, float fontRenderingEmSize, int length)
- => new SKGlyphRunBuffer(glyphTypeface, fontRenderingEmSize, length);
+ public IGlyphRunImpl CreateGlyphRun(IGlyphTypeface glyphTypeface, double fontRenderingEmSize, IReadOnlyList glyphIndices,
+ IReadOnlyList glyphAdvances, IReadOnlyList glyphOffsets)
+ {
+ if (glyphTypeface == null)
+ {
+ throw new ArgumentNullException(nameof(glyphTypeface));
+ }
- public IHorizontalGlyphRunBuffer AllocateHorizontalGlyphRun(IGlyphTypeface glyphTypeface, float fontRenderingEmSize, int length)
- => new SKHorizontalGlyphRunBuffer(glyphTypeface, fontRenderingEmSize, length);
+ if (glyphIndices == null)
+ {
+ throw new ArgumentNullException(nameof(glyphIndices));
+ }
- public IPositionedGlyphRunBuffer AllocatePositionedGlyphRun(IGlyphTypeface glyphTypeface, float fontRenderingEmSize, int length)
- => new SKPositionedGlyphRunBuffer(glyphTypeface, fontRenderingEmSize, length);
+ var glyphTypefaceImpl = glyphTypeface as GlyphTypefaceImpl;
- private abstract class SKGlyphRunBufferBase : IGlyphRunBuffer
- {
- protected readonly SKTextBlobBuilder _builder;
- protected readonly SKFont _font;
+ var font = new SKFont
+ {
+ LinearMetrics = true,
+ Subpixel = true,
+ Edging = SKFontEdging.SubpixelAntialias,
+ Hinting = SKFontHinting.Full,
+ Size = (float)fontRenderingEmSize,
+ Typeface = glyphTypefaceImpl.Typeface,
+ Embolden = (glyphTypefaceImpl.FontSimulations & FontSimulations.Bold) != 0,
+ SkewX = (glyphTypefaceImpl.FontSimulations & FontSimulations.Oblique) != 0 ? -0.2f : 0
+ };
+
+ var builder = new SKTextBlobBuilder();
- public SKGlyphRunBufferBase(IGlyphTypeface glyphTypeface, float fontRenderingEmSize, int length)
+ var count = glyphIndices.Count;
+
+ if(glyphOffsets != null && glyphAdvances != null)
{
- _builder = new SKTextBlobBuilder();
+ var runBuffer = builder.AllocatePositionedRun(font, count);
- var glyphTypefaceImpl = (GlyphTypefaceImpl)glyphTypeface;
+ var glyphSpan = runBuffer.GetGlyphSpan();
+ var positionSpan = runBuffer.GetPositionSpan();
- _font = new SKFont
- {
- Subpixel = true,
- Edging = SKFontEdging.SubpixelAntialias,
- Hinting = SKFontHinting.Full,
- LinearMetrics = true,
- Size = fontRenderingEmSize,
- Typeface = glyphTypefaceImpl.Typeface,
- Embolden = glyphTypefaceImpl.IsFakeBold,
- SkewX = glyphTypefaceImpl.IsFakeItalic ? -0.2f : 0
- };
- }
+ var currentX = 0.0;
- public abstract Span GlyphIndices { get; }
+ for (int i = 0; i < glyphOffsets.Count; i++)
+ {
+ var offset = glyphOffsets[i];
- public IGlyphRunImpl Build()
- {
- return new GlyphRunImpl(_builder.Build());
- }
- }
+ glyphSpan[i] = glyphIndices[i];
- private sealed class SKGlyphRunBuffer : SKGlyphRunBufferBase
- {
- private readonly SKRunBuffer _buffer;
+ positionSpan[i] = new SKPoint((float)(currentX + offset.X), (float)offset.Y);
- public SKGlyphRunBuffer(IGlyphTypeface glyphTypeface, float fontRenderingEmSize, int length) : base(glyphTypeface, fontRenderingEmSize, length)
- {
- _buffer = _builder.AllocateRun(_font, length, 0, 0);
+ currentX += glyphAdvances[i];
+ }
}
+ else
+ {
+ if(glyphAdvances != null)
+ {
+ var runBuffer = builder.AllocateHorizontalRun(font, count, 0);
- public override Span GlyphIndices => _buffer.GetGlyphSpan();
- }
+ var glyphSpan = runBuffer.GetGlyphSpan();
+ var positionSpan = runBuffer.GetPositionSpan();
- private sealed class SKHorizontalGlyphRunBuffer : SKGlyphRunBufferBase, IHorizontalGlyphRunBuffer
- {
- private readonly SKHorizontalRunBuffer _buffer;
+ var currentX = 0.0;
- public SKHorizontalGlyphRunBuffer(IGlyphTypeface glyphTypeface, float fontRenderingEmSize, int length) : base(glyphTypeface, fontRenderingEmSize, length)
- {
- _buffer = _builder.AllocateHorizontalRun(_font, length, 0);
- }
+ for (int i = 0; i < glyphOffsets.Count; i++)
+ {
+ glyphSpan[i] = glyphIndices[i];
- public override Span GlyphIndices => _buffer.GetGlyphSpan();
+ positionSpan[i] = (float)currentX;
- public Span GlyphPositions => _buffer.GetPositionSpan();
- }
+ currentX += glyphAdvances[i];
+ }
+ }
+ else
+ {
+ var runBuffer = builder.AllocateRun(font, count, 0, 0);
- private sealed class SKPositionedGlyphRunBuffer : SKGlyphRunBufferBase, IPositionedGlyphRunBuffer
- {
- private readonly SKPositionedRunBuffer _buffer;
+ var glyphSpan = runBuffer.GetGlyphSpan();
- public SKPositionedGlyphRunBuffer(IGlyphTypeface glyphTypeface, float fontRenderingEmSize, int length) : base(glyphTypeface, fontRenderingEmSize, length)
- {
- _buffer = _builder.AllocatePositionedRun(_font, length);
+ for (int i = 0; i < glyphOffsets.Count; i++)
+ {
+ glyphSpan[i] = glyphIndices[i];
+ }
+ }
}
- public override Span GlyphIndices => _buffer.GetGlyphSpan();
-
- public Span GlyphPositions => MemoryMarshal.Cast(_buffer.GetPositionSpan());
+ return new GlyphRunImpl(builder.Build());
}
}
}
diff --git a/src/Skia/Avalonia.Skia/SurfaceRenderTarget.cs b/src/Skia/Avalonia.Skia/SurfaceRenderTarget.cs
index 01b7449b64..d3231c92a5 100644
--- a/src/Skia/Avalonia.Skia/SurfaceRenderTarget.cs
+++ b/src/Skia/Avalonia.Skia/SurfaceRenderTarget.cs
@@ -116,20 +116,20 @@ namespace Avalonia.Skia
public int Version { get; private set; } = 1;
///
- public void Save(string fileName)
+ public void Save(string fileName, int? quality = null)
{
using (var image = SnapshotImage())
{
- ImageSavingHelper.SaveImage(image, fileName);
+ ImageSavingHelper.SaveImage(image, fileName, quality);
}
}
///
- public void Save(Stream stream)
+ public void Save(Stream stream, int? quality = null)
{
using (var image = SnapshotImage())
{
- ImageSavingHelper.SaveImage(image, stream);
+ ImageSavingHelper.SaveImage(image, stream, quality);
}
}
diff --git a/src/Skia/Avalonia.Skia/TextShaperImpl.cs b/src/Skia/Avalonia.Skia/TextShaperImpl.cs
index b07deb1f4d..eaf588c27d 100644
--- a/src/Skia/Avalonia.Skia/TextShaperImpl.cs
+++ b/src/Skia/Avalonia.Skia/TextShaperImpl.cs
@@ -60,11 +60,11 @@ namespace Avalonia.Skia
var glyphCluster = (int)(sourceInfo.Cluster);
- var glyphAdvance = GetGlyphAdvance(glyphPositions, i, textScale);
+ var glyphAdvance = GetGlyphAdvance(glyphPositions, i, textScale) + options.LetterSpacing;
var glyphOffset = GetGlyphOffset(glyphPositions, i, textScale);
- if(glyphIndex == 0 && text.Buffer.Span[glyphCluster] == '\t')
+ if(text.Buffer.Span[glyphCluster] == '\t')
{
glyphIndex = typeface.GetGlyph(' ');
diff --git a/src/Skia/Avalonia.Skia/WriteableBitmapImpl.cs b/src/Skia/Avalonia.Skia/WriteableBitmapImpl.cs
index 80bfcc5973..d437f514bb 100644
--- a/src/Skia/Avalonia.Skia/WriteableBitmapImpl.cs
+++ b/src/Skia/Avalonia.Skia/WriteableBitmapImpl.cs
@@ -137,20 +137,20 @@ namespace Avalonia.Skia
}
///
- public void Save(Stream stream)
+ public void Save(Stream stream, int? quality = null)
{
using (var image = GetSnapshot())
{
- ImageSavingHelper.SaveImage(image, stream);
+ ImageSavingHelper.SaveImage(image, stream, quality);
}
}
///
- public void Save(string fileName)
+ public void Save(string fileName, int? quality = null)
{
using (var image = GetSnapshot())
{
- ImageSavingHelper.SaveImage(image, fileName);
+ ImageSavingHelper.SaveImage(image, fileName, quality);
}
}
diff --git a/src/Web/Avalonia.Web/Avalonia.Web.csproj b/src/Web/Avalonia.Web/Avalonia.Web.csproj
index cdfa095865..88b23cdad2 100644
--- a/src/Web/Avalonia.Web/Avalonia.Web.csproj
+++ b/src/Web/Avalonia.Web/Avalonia.Web.csproj
@@ -7,6 +7,7 @@
+
diff --git a/src/Web/Avalonia.Web/Avalonia.Web.props b/src/Web/Avalonia.Web/Avalonia.Web.props
index 6c975cd284..668dd20789 100644
--- a/src/Web/Avalonia.Web/Avalonia.Web.props
+++ b/src/Web/Avalonia.Web/Avalonia.Web.props
@@ -1,5 +1,5 @@
-
+
- $(EmccExtraLDFlags) --js-library="$(MSBuildThisFileDirectory)\interop.js"
+ 16384000
diff --git a/src/Web/Avalonia.Web/Avalonia.Web.targets b/src/Web/Avalonia.Web/Avalonia.Web.targets
index d1bec2aa93..b6a09b33ef 100644
--- a/src/Web/Avalonia.Web/Avalonia.Web.targets
+++ b/src/Web/Avalonia.Web/Avalonia.Web.targets
@@ -4,4 +4,34 @@
+
+
+ True
+ $(EmccExtraLDFlags) --js-library="$(MSBuildThisFileDirectory)\interop.js"
+ $(EmccExtraLDFlags) -sERROR_ON_UNDEFINED_SYMBOLS=0
+ true
+
+
+
+ true
+ full
+ true
+ -Oz
+ -Oz
+ false
+ false
+ 0
+ false
+ true
+ false
+ false
+ false
+ false
+ false
+ false
+ true
+ true
+ en
+ false
+
diff --git a/src/Web/Avalonia.Web/AvaloniaView.cs b/src/Web/Avalonia.Web/AvaloniaView.cs
index 098b06a0a2..37614399ee 100644
--- a/src/Web/Avalonia.Web/AvaloniaView.cs
+++ b/src/Web/Avalonia.Web/AvaloniaView.cs
@@ -1,5 +1,9 @@
using System;
+using System.Collections.Generic;
+using System.Reflection;
using System.Runtime.InteropServices.JavaScript;
+
+using Avalonia.Collections.Pooled;
using Avalonia.Controls;
using Avalonia.Controls.Embedding;
using Avalonia.Controls.Platform;
@@ -18,6 +22,7 @@ namespace Avalonia.Web
[System.Runtime.Versioning.SupportedOSPlatform("browser")] // gets rid of callsite warnings
public partial class AvaloniaView : ITextInputMethodImpl
{
+ private static readonly PooledList s_intermediatePointsPooledList = new(ClearMode.Never);
private readonly BrowserTopLevelImpl _topLevelImpl;
private EmbeddableControlRoot _topLevel;
@@ -52,13 +57,13 @@ namespace Avalonia.Web
}
_containerElement = hostContent.GetPropertyAsJSObject("host")
- ?? throw new InvalidOperationException("Host cannot be null");
+ ?? throw new InvalidOperationException("Host cannot be null");
_canvas = hostContent.GetPropertyAsJSObject("canvas")
- ?? throw new InvalidOperationException("Canvas cannot be null");
+ ?? throw new InvalidOperationException("Canvas cannot be null");
_nativeControlsContainer = hostContent.GetPropertyAsJSObject("nativeHost")
- ?? throw new InvalidOperationException("NativeHost cannot be null");
+ ?? throw new InvalidOperationException("NativeHost cannot be null");
_inputElement = hostContent.GetPropertyAsJSObject("inputElement")
- ?? throw new InvalidOperationException("InputElement cannot be null");
+ ?? throw new InvalidOperationException("InputElement cannot be null");
_splash = DomHelper.GetElementById("avalonia-splash");
@@ -96,7 +101,8 @@ namespace Avalonia.Web
OnCompositionUpdate,
OnCompositionEnd);
- InputHelper.SubscribePointerEvents(_containerElement, OnPointerMove, OnPointerDown, OnPointerUp, OnWheel);
+ InputHelper.SubscribePointerEvents(_containerElement, OnPointerMove, OnPointerDown, OnPointerUp,
+ OnPointerCancel, OnWheel);
var skiaOptions = AvaloniaLocator.Current.GetService();
@@ -117,7 +123,12 @@ namespace Avalonia.Web
_context.SetResourceCacheLimit(skiaOptions?.MaxGpuResourceSizeBytes ?? 32 * 1024 * 1024);
}
- _topLevelImpl.Surfaces = new[] { new BrowserSkiaSurface(_context, _jsGlInfo, ColorType, new PixelSize((int)_canvasSize.Width, (int)_canvasSize.Height), _dpi, GRSurfaceOrigin.BottomLeft) };
+ _topLevelImpl.Surfaces = new[]
+ {
+ new BrowserSkiaSurface(_context, _jsGlInfo, ColorType,
+ new PixelSize((int)_canvasSize.Width, (int)_canvasSize.Height), _dpi,
+ GRSurfaceOrigin.BottomLeft)
+ };
}
else
{
@@ -135,7 +146,7 @@ namespace Avalonia.Web
DomHelper.ObserveSize(host, null, OnSizeChanged);
CanvasHelper.RequestAnimationFrame(_canvas, true);
-
+
InputHelper.FocusElement(_containerElement);
}
@@ -155,17 +166,36 @@ namespace Avalonia.Web
private bool OnPointerMove(JSObject args)
{
- var type = args.GetPropertyAsString("pointertype");
-
+ var pointerType = args.GetPropertyAsString("pointerType");
var point = ExtractRawPointerFromJSArgs(args);
+ var type = pointerType switch
+ {
+ "touch" => RawPointerEventType.TouchUpdate,
+ _ => RawPointerEventType.Move
+ };
+
+ var coalescedEvents = new Lazy?>(() =>
+ {
+ var points = InputHelper.GetCoalescedEvents(args);
+ s_intermediatePointsPooledList.Clear();
+ s_intermediatePointsPooledList.Capacity = points.Length - 1;
+
+ // Skip the last one, as it is already processed point.
+ for (var i = 0; i < points.Length - 1; i++)
+ {
+ var point = points[i];
+ s_intermediatePointsPooledList.Add(ExtractRawPointerFromJSArgs(point));
+ }
+
+ return s_intermediatePointsPooledList;
+ });
- return _topLevelImpl.RawPointerEvent(RawPointerEventType.Move, type!, point, GetModifiers(args), args.GetPropertyAsInt32("pointerId"));
+ return _topLevelImpl.RawPointerEvent(type, pointerType!, point, GetModifiers(args), args.GetPropertyAsInt32("pointerId"), coalescedEvents);
}
private bool OnPointerDown(JSObject args)
{
- var pointerType = args.GetPropertyAsString("pointerType");
-
+ var pointerType = args.GetPropertyAsString("pointerType") ?? "mouse";
var type = pointerType switch
{
"touch" => RawPointerEventType.TouchBegin,
@@ -176,20 +206,18 @@ namespace Avalonia.Web
2 => RawPointerEventType.RightButtonDown,
3 => RawPointerEventType.XButton1Down,
4 => RawPointerEventType.XButton2Down,
- // 5 => Pen eraser button,
+ 5 => RawPointerEventType.XButton1Down, // should be pen eraser button,
_ => RawPointerEventType.Move
}
};
var point = ExtractRawPointerFromJSArgs(args);
-
- return _topLevelImpl.RawPointerEvent(type, pointerType!, point, GetModifiers(args), args.GetPropertyAsInt32("pointerId"));
+ return _topLevelImpl.RawPointerEvent(type, pointerType, point, GetModifiers(args), args.GetPropertyAsInt32("pointerId"));
}
private bool OnPointerUp(JSObject args)
{
var pointerType = args.GetPropertyAsString("pointerType") ?? "mouse";
-
var type = pointerType switch
{
"touch" => RawPointerEventType.TouchEnd,
@@ -200,15 +228,27 @@ namespace Avalonia.Web
2 => RawPointerEventType.RightButtonUp,
3 => RawPointerEventType.XButton1Up,
4 => RawPointerEventType.XButton2Up,
- // 5 => Pen eraser button,
+ 5 => RawPointerEventType.XButton1Up, // should be pen eraser button,
_ => RawPointerEventType.Move
}
};
var point = ExtractRawPointerFromJSArgs(args);
-
return _topLevelImpl.RawPointerEvent(type, pointerType, point, GetModifiers(args), args.GetPropertyAsInt32("pointerId"));
}
+
+ private bool OnPointerCancel(JSObject args)
+ {
+ var pointerType = args.GetPropertyAsString("pointerType") ?? "mouse";
+ if (pointerType == "touch")
+ {
+ var point = ExtractRawPointerFromJSArgs(args);
+ _topLevelImpl.RawPointerEvent(RawPointerEventType.TouchCancel, pointerType, point,
+ GetModifiers(args), args.GetPropertyAsInt32("pointerId"));
+ }
+
+ return false;
+ }
private bool OnWheel(JSObject args)
{
diff --git a/src/Web/Avalonia.Web/BrowserTopLevelImpl.cs b/src/Web/Avalonia.Web/BrowserTopLevelImpl.cs
index b955da6df2..ed8f417870 100644
--- a/src/Web/Avalonia.Web/BrowserTopLevelImpl.cs
+++ b/src/Web/Avalonia.Web/BrowserTopLevelImpl.cs
@@ -67,17 +67,22 @@ namespace Avalonia.Web
public bool RawPointerEvent(
RawPointerEventType eventType, string pointerType,
- RawPointerPoint p, RawInputModifiers modifiers, long touchPointId)
+ RawPointerPoint p, RawInputModifiers modifiers, long touchPointId,
+ Lazy?>? intermediatePoints = null)
{
if (_inputRoot is { }
&& Input is { } input)
{
var device = GetPointerDevice(pointerType);
var args = device is TouchDevice ?
- new RawTouchEventArgs(device, Timestamp, _inputRoot, eventType, p, modifiers, touchPointId) :
+ new RawTouchEventArgs(device, Timestamp, _inputRoot, eventType, p, modifiers, touchPointId)
+ {
+ IntermediatePoints = intermediatePoints
+ } :
new RawPointerEventArgs(device, Timestamp, _inputRoot, eventType, p, modifiers)
{
- RawPointerId = touchPointId
+ RawPointerId = touchPointId,
+ IntermediatePoints = intermediatePoints
};
input.Invoke(args);
diff --git a/src/Web/Avalonia.Web/Interop/CanvasHelper.cs b/src/Web/Avalonia.Web/Interop/CanvasHelper.cs
index efa94916fa..5bbe503bc1 100644
--- a/src/Web/Avalonia.Web/Interop/CanvasHelper.cs
+++ b/src/Web/Avalonia.Web/Interop/CanvasHelper.cs
@@ -33,7 +33,7 @@ internal static partial class CanvasHelper
public static partial void RequestAnimationFrame(JSObject canvas, bool renderLoop);
[JSImport("Canvas.setCanvasSize", AvaloniaModule.MainModuleName)]
- public static partial void SetCanvasSize(JSObject canvas, int height, int width);
+ public static partial void SetCanvasSize(JSObject canvas, int width, int height);
[JSImport("Canvas.initGL", AvaloniaModule.MainModuleName)]
private static partial JSObject InitGL(
diff --git a/src/Web/Avalonia.Web/Interop/InputHelper.cs b/src/Web/Avalonia.Web/Interop/InputHelper.cs
index cfec9f30dc..904fa915a8 100644
--- a/src/Web/Avalonia.Web/Interop/InputHelper.cs
+++ b/src/Web/Avalonia.Web/Interop/InputHelper.cs
@@ -1,4 +1,5 @@
using System;
+using System.Runtime.InteropServices;
using System.Runtime.InteropServices.JavaScript;
using System.Threading.Tasks;
@@ -36,6 +37,8 @@ internal static partial class InputHelper
[JSMarshalAs>]
Func pointerUp,
[JSMarshalAs>]
+ Func pointerCancel,
+ [JSMarshalAs>]
Func wheel);
@@ -45,6 +48,9 @@ internal static partial class InputHelper
[JSMarshalAs>]
Func input);
+ [JSImport("InputHelper.getCoalescedEvents", AvaloniaModule.MainModuleName)]
+ [return: JSMarshalAs>]
+ public static partial JSObject[] GetCoalescedEvents(JSObject pointerEvent);
[JSImport("InputHelper.clearInput", AvaloniaModule.MainModuleName)]
public static partial void ClearInputElement(JSObject htmlElement);
diff --git a/src/Web/Avalonia.Web/webapp/modules/avalonia/input.ts b/src/Web/Avalonia.Web/webapp/modules/avalonia/input.ts
index ddc1f54ae7..83e8ee7f1c 100644
--- a/src/Web/Avalonia.Web/webapp/modules/avalonia/input.ts
+++ b/src/Web/Avalonia.Web/webapp/modules/avalonia/input.ts
@@ -95,41 +95,45 @@ export class InputHelper {
pointerMoveCallback: (args: PointerEvent) => boolean,
pointerDownCallback: (args: PointerEvent) => boolean,
pointerUpCallback: (args: PointerEvent) => boolean,
+ pointerCancelCallback: (args: PointerEvent) => boolean,
wheelCallback: (args: WheelEvent) => boolean
) {
const pointerMoveHandler = (args: PointerEvent) => {
- if (pointerMoveCallback(args)) {
- args.preventDefault();
- }
+ pointerMoveCallback(args);
+ args.preventDefault();
};
const pointerDownHandler = (args: PointerEvent) => {
- if (pointerDownCallback(args)) {
- args.preventDefault();
- }
+ pointerDownCallback(args);
+ args.preventDefault();
};
const pointerUpHandler = (args: PointerEvent) => {
- if (pointerUpCallback(args)) {
- args.preventDefault();
- }
+ pointerUpCallback(args);
+ args.preventDefault();
+ };
+
+ const pointerCancelHandler = (args: PointerEvent) => {
+ pointerCancelCallback(args);
+ args.preventDefault();
};
const wheelHandler = (args: WheelEvent) => {
- if (wheelCallback(args)) {
- args.preventDefault();
- }
+ wheelCallback(args);
+ args.preventDefault();
};
element.addEventListener("pointermove", pointerMoveHandler);
element.addEventListener("pointerdown", pointerDownHandler);
element.addEventListener("pointerup", pointerUpHandler);
element.addEventListener("wheel", wheelHandler);
+ element.addEventListener("pointercancel", pointerCancelHandler);
return () => {
element.removeEventListener("pointerover", pointerMoveHandler);
element.removeEventListener("pointerdown", pointerDownHandler);
element.removeEventListener("pointerup", pointerUpHandler);
+ element.removeEventListener("pointercancel", pointerCancelHandler);
element.removeEventListener("wheel", wheelHandler);
};
}
@@ -150,6 +154,10 @@ export class InputHelper {
};
}
+ public static getCoalescedEvents(pointerEvent: PointerEvent): PointerEvent[] {
+ return pointerEvent.getCoalescedEvents();
+ }
+
public static clearInput(inputElement: HTMLInputElement) {
inputElement.value = "";
}
diff --git a/src/Windows/Avalonia.Direct2D1/Direct2D1Platform.cs b/src/Windows/Avalonia.Direct2D1/Direct2D1Platform.cs
index 8103f89dad..4d307c9762 100644
--- a/src/Windows/Avalonia.Direct2D1/Direct2D1Platform.cs
+++ b/src/Windows/Avalonia.Direct2D1/Direct2D1Platform.cs
@@ -10,10 +10,7 @@ using Avalonia.Media.Imaging;
using Avalonia.Platform;
using SharpDX.DirectWrite;
using GlyphRun = Avalonia.Media.GlyphRun;
-using TextAlignment = Avalonia.Media.TextAlignment;
using SharpDX.Mathematics.Interop;
-using System.Runtime.InteropServices;
-using System.Drawing;
namespace Avalonia
{
@@ -160,6 +157,72 @@ namespace Avalonia.Direct2D1
public IGeometryImpl CreateGeometryGroup(FillRule fillRule, IReadOnlyList children) => new GeometryGroupImpl(fillRule, children);
public IGeometryImpl CreateCombinedGeometry(GeometryCombineMode combineMode, Geometry g1, Geometry g2) => new CombinedGeometryImpl(combineMode, g1, g2);
+ public IGlyphRunImpl CreateGlyphRun(IGlyphTypeface glyphTypeface, double fontRenderingEmSize, IReadOnlyList glyphIndices,
+ IReadOnlyList glyphAdvances, IReadOnlyList glyphOffsets)
+ {
+ var glyphTypefaceImpl = (GlyphTypefaceImpl)glyphTypeface;
+
+ var glyphCount = glyphIndices.Count;
+
+ var run = new SharpDX.DirectWrite.GlyphRun
+ {
+ FontFace = glyphTypefaceImpl.FontFace,
+ FontSize = (float)fontRenderingEmSize
+ };
+
+ var indices = new short[glyphCount];
+
+ for (var i = 0; i < glyphCount; i++)
+ {
+ indices[i] = (short)glyphIndices[i];
+ }
+
+ run.Indices = indices;
+
+ run.Advances = new float[glyphCount];
+
+ var scale = (float)(fontRenderingEmSize / glyphTypeface.Metrics.DesignEmHeight);
+
+ if (glyphAdvances == null)
+ {
+ for (var i = 0; i < glyphCount; i++)
+ {
+ var advance = glyphTypeface.GetGlyphAdvance(glyphIndices[i]) * scale;
+
+ run.Advances[i] = advance;
+ }
+ }
+ else
+ {
+ for (var i = 0; i < glyphCount; i++)
+ {
+ var advance = (float)glyphAdvances[i];
+
+ run.Advances[i] = advance;
+ }
+ }
+
+ if (glyphOffsets == null)
+ {
+ return new GlyphRunImpl(run);
+ }
+
+ run.Offsets = new GlyphOffset[glyphCount];
+
+ for (var i = 0; i < glyphCount; i++)
+ {
+ var (x, y) = glyphOffsets[i];
+
+ run.Offsets[i] = new GlyphOffset
+ {
+ AdvanceOffset = (float)x,
+ AscenderOffset = (float)y
+ };
+ }
+
+ return new GlyphRunImpl(run);
+ }
+
public IGeometryImpl BuildGlyphRunGeometry(GlyphRun glyphRun)
{
if (glyphRun.GlyphTypeface is not GlyphTypefaceImpl glyphTypeface)
@@ -260,68 +323,6 @@ namespace Avalonia.Direct2D1
return new WicBitmapImpl(format, alphaFormat, data, size, dpi, stride);
}
- private class DWGlyphRunBuffer : IGlyphRunBuffer
- {
- protected readonly SharpDX.DirectWrite.GlyphRun _dwRun;
-
- public DWGlyphRunBuffer(IGlyphTypeface glyphTypeface, float fontRenderingEmSize, int length)
- {
- var glyphTypefaceImpl = (GlyphTypefaceImpl)glyphTypeface;
-
- _dwRun = new SharpDX.DirectWrite.GlyphRun
- {
- FontFace = glyphTypefaceImpl.FontFace,
- FontSize = fontRenderingEmSize,
- Indices = new short[length]
- };
- }
-
- public Span GlyphIndices => MemoryMarshal.Cast(_dwRun.Indices.AsSpan());
-
- public IGlyphRunImpl Build()
- {
- return new GlyphRunImpl(_dwRun);
- }
- }
-
- private class DWHorizontalGlyphRunBuffer : DWGlyphRunBuffer, IHorizontalGlyphRunBuffer
- {
- public DWHorizontalGlyphRunBuffer(IGlyphTypeface glyphTypeface, float fontRenderingEmSize, int length)
- : base(glyphTypeface, fontRenderingEmSize, length)
- {
- _dwRun.Advances = new float[length];
- }
-
- public Span GlyphPositions => _dwRun.Advances.AsSpan();
- }
-
- private class DWPositionedGlyphRunBuffer : DWGlyphRunBuffer, IPositionedGlyphRunBuffer
- {
- public DWPositionedGlyphRunBuffer(IGlyphTypeface glyphTypeface, float fontRenderingEmSize, int length)
- : base(glyphTypeface, fontRenderingEmSize, length)
- {
- _dwRun.Advances = new float[length];
- _dwRun.Offsets = new GlyphOffset[length];
- }
-
- public Span GlyphPositions => MemoryMarshal.Cast(_dwRun.Offsets.AsSpan());
- }
-
- public IGlyphRunBuffer AllocateGlyphRun(IGlyphTypeface glyphTypeface, float fontRenderingEmSize, int length)
- {
- return new DWGlyphRunBuffer(glyphTypeface, fontRenderingEmSize, length);
- }
-
- public IHorizontalGlyphRunBuffer AllocateHorizontalGlyphRun(IGlyphTypeface glyphTypeface, float fontRenderingEmSize, int length)
- {
- return new DWHorizontalGlyphRunBuffer(glyphTypeface, fontRenderingEmSize, length);
- }
-
- public IPositionedGlyphRunBuffer AllocatePositionedGlyphRun(IGlyphTypeface glyphTypeface, float fontRenderingEmSize, int length)
- {
- return new DWPositionedGlyphRunBuffer(glyphTypeface, fontRenderingEmSize, length);
- }
-
public bool SupportsIndividualRoundRects => false;
public AlphaFormat DefaultAlphaFormat => AlphaFormat.Premul;
diff --git a/src/Windows/Avalonia.Direct2D1/Media/GlyphTypefaceImpl.cs b/src/Windows/Avalonia.Direct2D1/Media/GlyphTypefaceImpl.cs
index 77d0e58d3d..705c715455 100644
--- a/src/Windows/Avalonia.Direct2D1/Media/GlyphTypefaceImpl.cs
+++ b/src/Windows/Avalonia.Direct2D1/Media/GlyphTypefaceImpl.cs
@@ -1,10 +1,11 @@
using System;
-using System.Drawing.Drawing2D;
using Avalonia.Media;
using Avalonia.Metadata;
using HarfBuzzSharp;
using SharpDX.DirectWrite;
using FontMetrics = Avalonia.Media.FontMetrics;
+using FontSimulations = Avalonia.Media.FontSimulations;
+using GlyphMetrics = Avalonia.Media.GlyphMetrics;
namespace Avalonia.Direct2D1.Media
{
@@ -82,6 +83,8 @@ namespace Avalonia.Direct2D1.Media
public int GlyphCount { get; set; }
+ public FontSimulations FontSimulations => FontSimulations.None;
+
///
public ushort GetGlyph(uint codepoint)
{
@@ -135,6 +138,26 @@ namespace Avalonia.Direct2D1.Media
return Font.GetHorizontalGlyphAdvances(glyphIndices);
}
+ public bool TryGetGlyphMetrics(ushort glyph, out GlyphMetrics metrics)
+ {
+ metrics = default;
+
+ if (!Font.TryGetGlyphExtents(glyph, out var extents))
+ {
+ return false;
+ }
+
+ metrics = new GlyphMetrics
+ {
+ XBearing = extents.XBearing,
+ YBearing = extents.YBearing,
+ Width = extents.Width,
+ Height = extents.Height
+ };
+
+ return true;
+ }
+
private void Dispose(bool disposing)
{
if (_isDisposed)
diff --git a/src/Windows/Avalonia.Direct2D1/Media/Imaging/BitmapImpl.cs b/src/Windows/Avalonia.Direct2D1/Media/Imaging/BitmapImpl.cs
index 843efe2cc4..059105c112 100644
--- a/src/Windows/Avalonia.Direct2D1/Media/Imaging/BitmapImpl.cs
+++ b/src/Windows/Avalonia.Direct2D1/Media/Imaging/BitmapImpl.cs
@@ -15,7 +15,7 @@ namespace Avalonia.Direct2D1.Media
public abstract OptionalDispose GetDirect2DBitmap(SharpDX.Direct2D1.RenderTarget target);
- public void Save(string fileName)
+ public void Save(string fileName, int? quality = null)
{
if (Path.GetExtension(fileName) != ".png")
{
@@ -25,11 +25,11 @@ namespace Avalonia.Direct2D1.Media
using (FileStream s = new FileStream(fileName, FileMode.Create))
{
- Save(s);
+ Save(s, quality);
}
}
- public abstract void Save(Stream stream);
+ public abstract void Save(Stream stream, int? quality = null);
public virtual void Dispose()
{
diff --git a/src/Windows/Avalonia.Direct2D1/Media/Imaging/D2DBitmapImpl.cs b/src/Windows/Avalonia.Direct2D1/Media/Imaging/D2DBitmapImpl.cs
index 2656ab4c58..a321b225a0 100644
--- a/src/Windows/Avalonia.Direct2D1/Media/Imaging/D2DBitmapImpl.cs
+++ b/src/Windows/Avalonia.Direct2D1/Media/Imaging/D2DBitmapImpl.cs
@@ -43,7 +43,7 @@ namespace Avalonia.Direct2D1.Media
return new OptionalDispose(_direct2DBitmap, false);
}
- public override void Save(Stream stream)
+ public override void Save(Stream stream, int? quality = null)
{
using (var encoder = new PngBitmapEncoder(Direct2D1Platform.ImagingFactory, stream))
using (var frame = new BitmapFrameEncode(encoder))
diff --git a/src/Windows/Avalonia.Direct2D1/Media/Imaging/D2DRenderTargetBitmapImpl.cs b/src/Windows/Avalonia.Direct2D1/Media/Imaging/D2DRenderTargetBitmapImpl.cs
index 357e472d34..c5f8e837ce 100644
--- a/src/Windows/Avalonia.Direct2D1/Media/Imaging/D2DRenderTargetBitmapImpl.cs
+++ b/src/Windows/Avalonia.Direct2D1/Media/Imaging/D2DRenderTargetBitmapImpl.cs
@@ -56,7 +56,7 @@ namespace Avalonia.Direct2D1.Media.Imaging
return new OptionalDispose(_renderTarget.Bitmap, false);
}
- public override void Save(Stream stream)
+ public override void Save(Stream stream, int? quality = null)
{
using (var wic = new WicRenderTargetBitmapImpl(PixelSize, Dpi))
{
diff --git a/src/Windows/Avalonia.Direct2D1/Media/Imaging/WicBitmapImpl.cs b/src/Windows/Avalonia.Direct2D1/Media/Imaging/WicBitmapImpl.cs
index 1156246b29..051790ef03 100644
--- a/src/Windows/Avalonia.Direct2D1/Media/Imaging/WicBitmapImpl.cs
+++ b/src/Windows/Avalonia.Direct2D1/Media/Imaging/WicBitmapImpl.cs
@@ -186,7 +186,7 @@ namespace Avalonia.Direct2D1.Media
return new OptionalDispose(D2DBitmap.FromWicBitmap(renderTarget, converter), true);
}
- public override void Save(Stream stream)
+ public override void Save(Stream stream, int? quality = null)
{
using (var encoder = new PngBitmapEncoder(Direct2D1Platform.ImagingFactory, stream))
using (var frame = new BitmapFrameEncode(encoder))
diff --git a/src/Windows/Avalonia.Direct2D1/Media/TextShaperImpl.cs b/src/Windows/Avalonia.Direct2D1/Media/TextShaperImpl.cs
index 064320f809..7f2cbc6182 100644
--- a/src/Windows/Avalonia.Direct2D1/Media/TextShaperImpl.cs
+++ b/src/Windows/Avalonia.Direct2D1/Media/TextShaperImpl.cs
@@ -64,7 +64,7 @@ namespace Avalonia.Direct2D1.Media
var glyphOffset = GetGlyphOffset(glyphPositions, i, textScale);
- if (glyphIndex == 0 && text.Buffer.Span[glyphCluster] == '\t')
+ if (text.Buffer.Span[glyphCluster] == '\t')
{
glyphIndex = typeface.GetGlyph(' ');
diff --git a/src/Windows/Avalonia.Win32/Avalonia.Win32.csproj b/src/Windows/Avalonia.Win32/Avalonia.Win32.csproj
index 0d29bb91ea..308e22b4e2 100644
--- a/src/Windows/Avalonia.Win32/Avalonia.Win32.csproj
+++ b/src/Windows/Avalonia.Win32/Avalonia.Win32.csproj
@@ -11,7 +11,7 @@
-
+
diff --git a/src/Windows/Avalonia.Win32/Interop/UnmanagedMethods.cs b/src/Windows/Avalonia.Win32/Interop/UnmanagedMethods.cs
index f1ed53ea99..4afb1068df 100644
--- a/src/Windows/Avalonia.Win32/Interop/UnmanagedMethods.cs
+++ b/src/Windows/Avalonia.Win32/Interop/UnmanagedMethods.cs
@@ -2104,6 +2104,12 @@ namespace Avalonia.Win32.Interop
public int Y;
}
+ public struct SIZE_F
+ {
+ public float X;
+ public float Y;
+ }
+
public struct RECT
{
public int left;
diff --git a/src/Windows/Avalonia.Win32/WinRT/Composition/WinUICompositorConnection.cs b/src/Windows/Avalonia.Win32/WinRT/Composition/WinUICompositorConnection.cs
index 8a41c00add..6da17b8ea5 100644
--- a/src/Windows/Avalonia.Win32/WinRT/Composition/WinUICompositorConnection.cs
+++ b/src/Windows/Avalonia.Win32/WinRT/Composition/WinUICompositorConnection.cs
@@ -174,8 +174,9 @@ namespace Avalonia.Win32.WinRT.Composition
using var sc = _syncContext.EnsureLocked();
using var desktopTarget = _compositorDesktopInterop.CreateDesktopWindowTarget(hWnd, 0);
using var target = desktopTarget.QueryInterface();
+ using var device2 = _device.QueryInterface();
- using var drawingSurface = _device.CreateDrawingSurface(new UnmanagedMethods.SIZE(), DirectXPixelFormat.B8G8R8A8UIntNormalized,
+ using var drawingSurface = device2.CreateDrawingSurface2(new UnmanagedMethods.SIZE(), DirectXPixelFormat.B8G8R8A8UIntNormalized,
DirectXAlphaMode.Premultiplied);
using var surface = drawingSurface.QueryInterface();
using var surfaceInterop = drawingSurface.QueryInterface();
diff --git a/src/Windows/Avalonia.Win32/WinRT/winrt.idl b/src/Windows/Avalonia.Win32/WinRT/winrt.idl
index 851df9dae6..ffb98fafc8 100644
--- a/src/Windows/Avalonia.Win32/WinRT/winrt.idl
+++ b/src/Windows/Avalonia.Win32/WinRT/winrt.idl
@@ -8,6 +8,7 @@
@clr-map Matrix4x4 System.Numerics.Matrix4x4
@clr-map RECT Avalonia.Win32.Interop.UnmanagedMethods.RECT
@clr-map SIZE Avalonia.Win32.Interop.UnmanagedMethods.SIZE
+@clr-map SIZE_F Avalonia.Win32.Interop.UnmanagedMethods.SIZE_F
@clr-map POINT Avalonia.Win32.Interop.UnmanagedMethods.POINT
@clr-map HWND IntPtr
@clr-map BOOL int
@@ -442,12 +443,18 @@ interface IDesktopWindowContentBridgeInterop : IUnknown
[uuid(FB22C6E1-80A2-4667-9936-DBEAF6EEFE95)]
interface ICompositionGraphicsDevice : IInspectable
{
- HRESULT CreateDrawingSurface([in] SIZE sizePixels, [in] DirectXPixelFormat pixelFormat,
+ HRESULT CreateDrawingSurface([in] SIZE_F sizePixels, [in] DirectXPixelFormat pixelFormat,
[in] DirectXAlphaMode alphaMode, [out] [retval] ICompositionDrawingSurface** result);
HRESULT AddRenderingDeviceReplaced(void* handler, void* token);
HRESULT RemoveRenderingDeviceReplaced([in] int token);
}
+[uuid(0FB8BDF6-C0F0-4BCC-9FB8-084982490D7D)]
+interface ICompositionGraphicsDevice2 : IInspectable
+{
+ HRESULT CreateDrawingSurface2([in] SIZE sizePixels, [in] DirectXPixelFormat pixelFormat, [in] DirectXAlphaMode alphaMode, [out] [retval] ICompositionDrawingSurface** result);
+}
+
[uuid(1527540D-42C7-47A6-A408-668F79A90DFB)]
interface ICompositionSurface : IInspectable
{
@@ -465,7 +472,7 @@ interface ICompositionDrawingSurface : IInspectable
{
[propget] HRESULT GetAlphaMode([out] [retval] DirectXAlphaMode* value);
[propget] HRESULT GetPixelFormat([out] [retval] DirectXPixelFormat* value);
- [propget] HRESULT GetSize([out] [retval] POINT* value);
+ [propget] HRESULT GetSize([out] [retval] SIZE_F* value);
}
enum CompositionBitmapInterpolationMode
diff --git a/src/iOS/Avalonia.iOS/AvaloniaView.cs b/src/iOS/Avalonia.iOS/AvaloniaView.cs
index 1c79e93f5c..2080352020 100644
--- a/src/iOS/Avalonia.iOS/AvaloniaView.cs
+++ b/src/iOS/Avalonia.iOS/AvaloniaView.cs
@@ -46,7 +46,6 @@ namespace Avalonia.iOS
);
_topLevelImpl.Surfaces = new[] { new EaglLayerSurface(l) };
MultipleTouchEnabled = true;
- AddSubviews(new UIView[] { new UIKit.UIButton(UIButtonType.InfoDark) });
}
public override bool CanBecomeFirstResponder => true;
diff --git a/tests/Avalonia.Base.UnitTests/VisualTree/MockRenderInterface.cs b/tests/Avalonia.Base.UnitTests/VisualTree/MockRenderInterface.cs
index 5f8eb45f71..10db08f302 100644
--- a/tests/Avalonia.Base.UnitTests/VisualTree/MockRenderInterface.cs
+++ b/tests/Avalonia.Base.UnitTests/VisualTree/MockRenderInterface.cs
@@ -72,7 +72,7 @@ namespace Avalonia.Base.UnitTests.VisualTree
throw new NotImplementedException();
}
- public IGlyphRunImpl CreateGlyphRun(GlyphRun glyphRun)
+ public IGlyphRunImpl CreateGlyphRun(IGlyphTypeface glyphTypeface, double fontRenderingEmSize, IReadOnlyList glyphIndices, IReadOnlyList glyphAdvances, IReadOnlyList glyphOffsets)
{
throw new NotImplementedException();
}
@@ -126,21 +126,6 @@ namespace Avalonia.Base.UnitTests.VisualTree
throw new NotImplementedException();
}
- public IGlyphRunBuffer AllocateGlyphRun(IGlyphTypeface glyphTypeface, float fontRenderingEmSize, int length)
- {
- throw new NotImplementedException();
- }
-
- public IHorizontalGlyphRunBuffer AllocateHorizontalGlyphRun(IGlyphTypeface glyphTypeface, float fontRenderingEmSize, int length)
- {
- throw new NotImplementedException();
- }
-
- public IPositionedGlyphRunBuffer AllocatePositionedGlyphRun(IGlyphTypeface glyphTypeface, float fontRenderingEmSize, int length)
- {
- throw new NotImplementedException();
- }
-
class MockStreamGeometry : IStreamGeometryImpl
{
private MockStreamGeometryContext _impl = new MockStreamGeometryContext();
diff --git a/tests/Avalonia.Benchmarks/NullRenderingPlatform.cs b/tests/Avalonia.Benchmarks/NullRenderingPlatform.cs
index 34f0dfef11..4170de71e6 100644
--- a/tests/Avalonia.Benchmarks/NullRenderingPlatform.cs
+++ b/tests/Avalonia.Benchmarks/NullRenderingPlatform.cs
@@ -5,6 +5,7 @@ using Avalonia.Media;
using Avalonia.Platform;
using Avalonia.UnitTests;
using Avalonia.Media.Imaging;
+using Microsoft.Diagnostics.Runtime;
namespace Avalonia.Benchmarks
{
@@ -117,19 +118,9 @@ namespace Avalonia.Benchmarks
return new MockStreamGeometryImpl();
}
- public IGlyphRunBuffer AllocateGlyphRun(IGlyphTypeface glyphTypeface, float fontRenderingEmSize, int length)
+ public IGlyphRunImpl CreateGlyphRun(IGlyphTypeface glyphTypeface, double fontRenderingEmSize, IReadOnlyList glyphIndices, IReadOnlyList glyphAdvances, IReadOnlyList glyphOffsets)
{
- throw new NotImplementedException();
- }
-
- public IHorizontalGlyphRunBuffer AllocateHorizontalGlyphRun(IGlyphTypeface glyphTypeface, float fontRenderingEmSize, int length)
- {
- throw new NotImplementedException();
- }
-
- public IPositionedGlyphRunBuffer AllocatePositionedGlyphRun(IGlyphTypeface glyphTypeface, float fontRenderingEmSize, int length)
- {
- throw new NotImplementedException();
+ return new MockGlyphRun();
}
public bool SupportsIndividualRoundRects => true;
diff --git a/tests/Avalonia.Controls.UnitTests/ListBoxTests_Multiple.cs b/tests/Avalonia.Controls.UnitTests/ListBoxTests_Multiple.cs
index 824d3ab979..47eca4fbcd 100644
--- a/tests/Avalonia.Controls.UnitTests/ListBoxTests_Multiple.cs
+++ b/tests/Avalonia.Controls.UnitTests/ListBoxTests_Multiple.cs
@@ -36,7 +36,7 @@ namespace Avalonia.Controls.UnitTests
}
[Fact]
- public void Focusing_Item_With_Ctrl_And_Arrow_Key_Should_Add_To_Selection()
+ public void Focusing_Item_With_Ctrl_And_Arrow_Key_Should_Not_Add_To_Selection()
{
var target = new ListBox
{
@@ -56,11 +56,11 @@ namespace Avalonia.Controls.UnitTests
KeyModifiers = KeyModifiers.Control
});
- Assert.Equal(new[] { "Foo", "Bar" }, target.SelectedItems);
+ Assert.Equal(new[] { "Foo" }, target.SelectedItems);
}
[Fact]
- public void Focusing_Selected_Item_With_Ctrl_And_Arrow_Key_Should_Remove_From_Selection()
+ public void Focusing_Selected_Item_With_Ctrl_And_Arrow_Key_Should_Not_Remove_From_Selection()
{
var target = new ListBox
{
@@ -81,7 +81,7 @@ namespace Avalonia.Controls.UnitTests
KeyModifiers = KeyModifiers.Control
});
- Assert.Equal(new[] { "Bar" }, target.SelectedItems);
+ Assert.Equal(new[] { "Foo", "Bar" }, target.SelectedItems);
}
private Control CreateListBoxTemplate(ITemplatedControl parent, INameScope scope)
diff --git a/tests/Avalonia.Controls.UnitTests/ListBoxTests_Single.cs b/tests/Avalonia.Controls.UnitTests/ListBoxTests_Single.cs
index c9ae3625cc..1081d22e5b 100644
--- a/tests/Avalonia.Controls.UnitTests/ListBoxTests_Single.cs
+++ b/tests/Avalonia.Controls.UnitTests/ListBoxTests_Single.cs
@@ -59,6 +59,58 @@ namespace Avalonia.Controls.UnitTests
Assert.Equal(0, target.SelectedIndex);
}
+ [Fact]
+ public void Focusing_Item_With_Arrow_Key_And_Ctrl_Pressed_Should_Not_Select_It()
+ {
+ var target = new ListBox
+ {
+ Template = new FuncControlTemplate(CreateListBoxTemplate),
+ Items = new[] { "Foo", "Bar", "Baz " },
+ };
+
+ ApplyTemplate(target);
+
+ target.Presenter.Panel.Children[0].RaiseEvent(new GotFocusEventArgs
+ {
+ RoutedEvent = InputElement.GotFocusEvent,
+ NavigationMethod = NavigationMethod.Directional,
+ KeyModifiers = KeyModifiers.Control
+ });
+
+ Assert.Equal(-1, target.SelectedIndex);
+ }
+
+ [Fact]
+ public void Pressing_Space_On_Focused_Item_With_Ctrl_Pressed_Should_Select_It()
+ {
+ using (UnitTestApplication.Start())
+ {
+ var target = new ListBox
+ {
+ Template = new FuncControlTemplate(CreateListBoxTemplate),
+ Items = new[] { "Foo", "Bar", "Baz " },
+ };
+ AvaloniaLocator.CurrentMutable.Bind().ToConstant(new Mock().Object);
+ ApplyTemplate(target);
+
+ target.Presenter.Panel.Children[0].RaiseEvent(new GotFocusEventArgs
+ {
+ RoutedEvent = InputElement.GotFocusEvent,
+ NavigationMethod = NavigationMethod.Directional,
+ KeyModifiers = KeyModifiers.Control
+ });
+
+ target.Presenter.Panel.Children[0].RaiseEvent(new KeyEventArgs
+ {
+ RoutedEvent = InputElement.KeyDownEvent,
+ Key = Key.Space,
+ KeyModifiers = KeyModifiers.Control
+ });
+
+ Assert.Equal(0, target.SelectedIndex);
+ }
+ }
+
[Fact]
public void Clicking_Item_Should_Select_It()
{
diff --git a/tests/Avalonia.Controls.UnitTests/RichTextBlockTests.cs b/tests/Avalonia.Controls.UnitTests/RichTextBlockTests.cs
deleted file mode 100644
index 05007e4f2e..0000000000
--- a/tests/Avalonia.Controls.UnitTests/RichTextBlockTests.cs
+++ /dev/null
@@ -1,132 +0,0 @@
-using Avalonia.Controls.Documents;
-using Avalonia.Controls.Presenters;
-using Avalonia.Controls.Templates;
-using Avalonia.Media;
-using Avalonia.UnitTests;
-using Xunit;
-
-namespace Avalonia.Controls.UnitTests
-{
- public class RichTextBlockTests
- {
- [Fact]
- public void Changing_InlinesCollection_Should_Invalidate_Measure()
- {
- using (UnitTestApplication.Start(TestServices.MockPlatformRenderInterface))
- {
- var target = new RichTextBlock();
-
- target.Measure(Size.Infinity);
-
- Assert.True(target.IsMeasureValid);
-
- target.Inlines.Add(new Run("Hello"));
-
- Assert.False(target.IsMeasureValid);
-
- target.Measure(Size.Infinity);
-
- Assert.True(target.IsMeasureValid);
- }
- }
-
- [Fact]
- public void Changing_Inlines_Properties_Should_Invalidate_Measure()
- {
- using (UnitTestApplication.Start(TestServices.MockPlatformRenderInterface))
- {
- var target = new RichTextBlock();
-
- var inline = new Run("Hello");
-
- target.Inlines.Add(inline);
-
- target.Measure(Size.Infinity);
-
- Assert.True(target.IsMeasureValid);
-
- inline.Foreground = Brushes.Green;
-
- Assert.False(target.IsMeasureValid);
- }
- }
-
- [Fact]
- public void Changing_Inlines_Should_Invalidate_Measure()
- {
- using (UnitTestApplication.Start(TestServices.MockPlatformRenderInterface))
- {
- var target = new RichTextBlock();
-
- var inlines = new InlineCollection { new Run("Hello") };
-
- target.Measure(Size.Infinity);
-
- Assert.True(target.IsMeasureValid);
-
- target.Inlines = inlines;
-
- Assert.False(target.IsMeasureValid);
- }
- }
-
- [Fact]
- public void Changing_Inlines_Should_Reset_Inlines_Parent()
- {
- using (UnitTestApplication.Start(TestServices.MockPlatformRenderInterface))
- {
- var target = new RichTextBlock();
-
- var run = new Run("Hello");
-
- target.Inlines.Add(run);
-
- target.Measure(Size.Infinity);
-
- Assert.True(target.IsMeasureValid);
-
- target.Inlines = null;
-
- Assert.Null(run.Parent);
-
- target.Inlines = new InlineCollection { run };
-
- Assert.Equal(target, run.Parent);
- }
- }
-
- [Fact]
- public void InlineUIContainer_Child_Schould_Be_Arranged()
- {
- using (UnitTestApplication.Start(TestServices.StyledWindow))
- {
- var target = new RichTextBlock();
-
- var button = new Button { Content = "12345678" };
-
- button.Template = new FuncControlTemplate