Skip to content

Commit 773efc4

Browse files
authored
Merge branch 'main' into fix/parallels-vulkan-fallback
2 parents e82364f + 59affb9 commit 773efc4

34 files changed

Lines changed: 1208 additions & 97 deletions

native/Avalonia.Native/src/OSX/AvnView.mm

Lines changed: 25 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -80,7 +80,17 @@ -(AvnView*) initWithParent: (TopLevelImpl*) parent
8080
[self setLayerContentsRedrawPolicy: NSViewLayerContentsRedrawDuringViewResize];
8181

8282
_parent = parent;
83-
_area = nullptr;
83+
84+
// NSTrackingInVisibleRect makes AppKit follow the visible bounds of the view.
85+
// Because of this, the tracking area does not need to change on resize.
86+
// Do not remove and add the area again on each live-resize tick.
87+
// If you do, AppKit queues synthetic enter and exit events with stale locations.
88+
// These events corrupt the pointer position after a resize and force it to the top left.
89+
NSTrackingAreaOptions options = NSTrackingActiveAlways | NSTrackingMouseMoved |
90+
NSTrackingMouseEnteredAndExited | NSTrackingEnabledDuringMouseDrag | NSTrackingInVisibleRect;
91+
_area = [[NSTrackingArea alloc] initWithRect:NSZeroRect options:options owner:self userInfo:nullptr];
92+
[self addTrackingArea:_area];
93+
8494
_lastPixelSize.Height = 100;
8595
_lastPixelSize.Width = 100;
8696
[self registerForDraggedTypes: @[@"public.data", GetAvnCustomDataType()]];
@@ -128,25 +138,12 @@ -(void)setFrameSize:(NSSize)newSize
128138
{
129139
[super setFrameSize:newSize];
130140

131-
if(_area != nullptr)
132-
{
133-
[self removeTrackingArea:_area];
134-
_area = nullptr;
135-
}
136-
137141
auto parent = _parent.tryGet();
138142
if (parent == nullptr)
139143
{
140144
return;
141145
}
142146

143-
NSRect rect = NSZeroRect;
144-
rect.size = newSize;
145-
146-
NSTrackingAreaOptions options = NSTrackingActiveAlways | NSTrackingMouseMoved | NSTrackingMouseEnteredAndExited | NSTrackingEnabledDuringMouseDrag;
147-
_area = [[NSTrackingArea alloc] initWithRect:rect options:options owner:self userInfo:nullptr];
148-
[self addTrackingArea:_area];
149-
150147
parent->UpdateCursor();
151148

152149
auto fsize = [self convertSizeToBacking: [self frame].size];
@@ -258,8 +255,12 @@ - (void)mouseEvent:(NSEvent *)event withType:(AvnRawMouseEventType) type
258255
return;
259256
}
260257

261-
NSPoint eventLocation = [event locationInWindow];
262-
258+
// AppKit synthesizes enter and exit events for the tracked geometry.
259+
// They can have stale locations. Use the current pointer location instead.
260+
NSPoint eventLocation = event.type == NSEventTypeMouseEntered || event.type == NSEventTypeMouseExited
261+
? [[self window] mouseLocationOutsideOfEventStream]
262+
: [event locationInWindow];
263+
263264
auto viewLocation = [self convertPoint:NSMakePoint(0, 0) toView:nil];
264265

265266
auto localPoint = NSMakePoint(eventLocation.x - viewLocation.x, viewLocation.y - eventLocation.y);
@@ -392,7 +393,10 @@ - (void)mouseEvent:(NSEvent *)event withType:(AvnRawMouseEventType) type
392393
parent->TopLevelEvents->RawMouseEvent(type, pointerType, timestamp, modifiers, point, delta, pressure, xTilt, yTilt);
393394
}
394395

395-
[super mouseMoved:event];
396+
// This handler is shared by all mouse event types. Only real mouse-moved
397+
// events must continue up the responder chain as mouseMoved:
398+
if (event.type == NSEventTypeMouseMoved)
399+
[super mouseMoved:event];
396400
}
397401

398402
- (BOOL) resignFirstResponder
@@ -560,7 +564,10 @@ - (void)swipeWithEvent:(NSEvent *)event
560564

561565
- (void)mouseEntered:(NSEvent *)event
562566
{
563-
[self mouseEvent:event withType:Move];
567+
// If the pointer is not inside the view, the enter event is false. Ignore it.
568+
NSPoint location = [self convertPoint:[[self window] mouseLocationOutsideOfEventStream] fromView:nil];
569+
if (NSPointInRect(location, self.bounds))
570+
[self mouseEvent:event withType:Move];
564571
[super mouseEntered:event];
565572
}
566573

native/Avalonia.Native/src/OSX/AvnWindow.mm

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -348,6 +348,10 @@ - (void)windowDidResize:(NSNotification *_Nonnull)notification
348348

349349
- (void)windowWillExitFullScreen:(NSNotification *_Nonnull)notification
350350
{
351+
// Prepare the destination appearance before AppKit starts the exit animation.
352+
if (_isExtended)
353+
[self setTitlebarAppearsTransparent:true];
354+
351355
auto parent = _parent.tryGetWithCast<IWindowStateChanged>();
352356

353357
if(parent != nullptr)

samples/GpuInterop/VulkanDemo/VulkanSwapchain.cs

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -87,9 +87,7 @@ public void BeginDraw()
8787
var buffer = _vk.Pool.CreateCommandBuffer();
8888
buffer.BeginRecording();
8989

90-
_image.TransitionLayout(buffer.InternalHandle,
91-
ImageLayout.Undefined, AccessFlags.None,
92-
ImageLayout.ColorAttachmentOptimal, AccessFlags.ColorAttachmentReadBit);
90+
_image.TransitionLayout(buffer.InternalHandle, ImageLayout.ColorAttachmentOptimal, AccessFlags.ColorAttachmentReadBit);
9391

9492
if(_image.IsDirectXBacked)
9593
buffer.Submit(null,null,null, null, new VulkanCommandBufferPool.VulkanCommandBuffer.KeyedMutexSubmitInfo
@@ -179,7 +177,11 @@ public void Present()
179177
Format = RuntimeInformation.IsOSPlatform(OSPlatform.OSX) ? PlatformGraphicsExternalImageFormat.B8G8R8A8UNorm : PlatformGraphicsExternalImageFormat.R8G8B8A8UNorm,
180178
Width = Size.Width,
181179
Height = Size.Height,
182-
MemorySize = _image.MemorySize
180+
MemorySize = _image.MemorySize,
181+
VulkanProperties = new PlatformGraphicsExternalImageVulkanProperties
182+
{
183+
Layout = (int)ImageLayout.TransferSrcOptimal
184+
}
183185
});
184186
if (_importedTimelineSemaphore != null)
185187
{

src/Android/Avalonia.Android/AvaloniaAccessHelper.cs

Lines changed: 19 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@
88
using Avalonia.Automation;
99
using Avalonia.Automation.Peers;
1010
using Avalonia.Automation.Provider;
11+
using Avalonia.Controls.Automation.Peers;
1112
using Java.Lang;
1213

1314
namespace Avalonia.Android
@@ -84,6 +85,8 @@ public AvaloniaAccessHelper(AvaloniaView view) : base(view)
8485
}
8586
}
8687

88+
private static bool IsInteropPeer(AutomationPeer peer) => peer is InteropAutomationPeer;
89+
8790
private HashSet<INodeInfoProvider> GetOrCreateNodeInfoProvidersFromPeer(AutomationPeer peer, out int virtualViewId)
8891
{
8992
int peerViewId;
@@ -175,6 +178,11 @@ protected override int GetVirtualViewAt(float x, float y)
175178
AutomationPeer? peer = embeddedRootProvider?.GetPeerFromPoint(p);
176179
if (peer is not null)
177180
{
181+
if (IsInteropPeer(peer))
182+
{
183+
return InvalidId;
184+
}
185+
178186
int virtualViewId;
179187
if (peer.GetParent() is AutomationPeer parent &&
180188
!s_containerTypes.Contains(parent.GetAutomationControlType()))
@@ -191,7 +199,7 @@ protected override int GetVirtualViewAt(float x, float y)
191199
else
192200
{
193201
peer = embeddedRootProvider?.GetFocus();
194-
return peer is null ? InvalidId : _peerIds[peer];
202+
return peer is null || IsInteropPeer(peer) ? InvalidId : _peerIds[peer];
195203
}
196204
}
197205

@@ -204,6 +212,11 @@ protected override void GetVisibleVirtualViews(IList<Integer>? virtualViewIds)
204212

205213
foreach (AutomationPeer peer in _peers[0].GetChildren())
206214
{
215+
if (IsInteropPeer(peer))
216+
{
217+
continue;
218+
}
219+
207220
GetOrCreateNodeInfoProvidersFromPeer(peer, out int virtualViewId);
208221
virtualViewIds.Add(Integer.ValueOf(virtualViewId));
209222
}
@@ -255,6 +268,11 @@ protected override void OnPopulateNodeForVirtualView(int virtualViewId, Accessib
255268
// UI logical structure
256269
foreach (AutomationPeer child in peer.GetChildren())
257270
{
271+
if (IsInteropPeer(child))
272+
{
273+
continue;
274+
}
275+
258276
GetOrCreateNodeInfoProvidersFromPeer(child, out int childId);
259277
nodeInfo.AddChild(_view, childId);
260278
}

src/Android/Avalonia.Android/Platform/Input/AndroidInputMethod.cs

Lines changed: 24 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -174,11 +174,14 @@ public void SetOptions(TextInputOptions options)
174174

175175
outAttrs.InputType = options.ContentType switch
176176
{
177-
TextInputContentType.Email => InputTypes.TextVariationEmailAddress,
178-
TextInputContentType.Number => InputTypes.ClassNumber,
179-
TextInputContentType.Password => InputTypes.TextVariationPassword,
177+
TextInputContentType.Email => InputTypes.ClassText | InputTypes.TextVariationEmailAddress,
178+
TextInputContentType.Number => InputTypes.ClassNumber | InputTypes.NumberFlagDecimal | InputTypes.NumberFlagSigned,
179+
TextInputContentType.Password => InputTypes.ClassText | InputTypes.TextVariationPassword,
180+
TextInputContentType.Pin => InputTypes.ClassNumber | InputTypes.NumberVariationPassword,
180181
TextInputContentType.Digits => InputTypes.ClassPhone,
181-
TextInputContentType.Url => InputTypes.TextVariationUri,
182+
TextInputContentType.Url => InputTypes.ClassText | InputTypes.TextVariationUri,
183+
TextInputContentType.Name => InputTypes.ClassText | InputTypes.TextVariationPersonName,
184+
// Alpha/Normal/Social/Search have no dedicated Android keyboard
182185
_ => InputTypes.ClassText
183186
};
184187

@@ -191,9 +194,6 @@ public void SetOptions(TextInputOptions options)
191194
if (options.Multiline)
192195
outAttrs.InputType |= InputTypes.TextFlagMultiLine;
193196

194-
if (outAttrs.InputType is InputTypes.ClassText && options.ShowSuggestions == false)
195-
outAttrs.InputType |= InputTypes.TextVariationPassword | InputTypes.TextFlagNoSuggestions;
196-
197197
outAttrs.ImeOptions = options.ReturnKeyType switch
198198
{
199199
TextInputReturnKeyType.Return => ImeFlags.NoEnterAction,
@@ -208,6 +208,23 @@ public void SetOptions(TextInputOptions options)
208208

209209
outAttrs.ImeOptions |= ImeFlags.NoFullscreen | ImeFlags.NoExtractUi;
210210

211+
if (options.ShowSuggestions == false &&
212+
(outAttrs.InputType & InputTypes.MaskClass) == InputTypes.ClassText)
213+
{
214+
outAttrs.InputType |= InputTypes.TextFlagNoSuggestions;
215+
216+
if ((outAttrs.InputType & InputTypes.MaskVariation) == InputTypes.TextVariationNormal)
217+
outAttrs.InputType |= InputTypes.TextVariationVisiblePassword;
218+
}
219+
220+
if (options.IsSensitive)
221+
{
222+
outAttrs.InputType |= InputTypes.TextFlagNoSuggestions;
223+
224+
if (OperatingSystem.IsAndroidVersionAtLeast(26))
225+
outAttrs.ImeOptions |= ImeFlags.NoPersonalizedLearning;
226+
}
227+
211228
if (options.LocaleHints?.Count > 0)
212229
outAttrs.HintLocales = new LocaleList(options.LocaleHints.Select(Java.Util.Locale.ForLanguageTag).ToArray());
213230

src/Avalonia.Base/Input/FocusManager.cs

Lines changed: 16 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -80,7 +80,22 @@ public bool Focus(
8080

8181
if (_focusRoot?.GetValue(FocusedElementProperty) is { } restore && restore != Current)
8282
{
83-
return FocusCore(keyboardDevice, restore, method, keyModifiers);
83+
if (!CanFocus(restore))
84+
{
85+
// Previous effective focus is no longer part of the focus root's visual tree. We clear the focused element
86+
_focusRoot.ClearValue(FocusedElementProperty);
87+
88+
if (Current != null && GetFocusScope(Current) != _focusRoot)
89+
{
90+
_focusRoot = null;
91+
92+
return false;
93+
}
94+
}
95+
else
96+
{
97+
return FocusCore(keyboardDevice, restore, method, keyModifiers);
98+
}
8499
}
85100

86101
_focusRoot = null;

src/Avalonia.Base/Input/InputElement.cs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -568,8 +568,8 @@ protected override void OnDetachedFromVisualTreeCore(VisualTreeAttachmentEventAr
568568
if (IsFocused)
569569
{
570570
var root = e.AttachmentPoint ?? e.RootVisual;
571-
((FocusManager?)e.PresentationSource.InputRoot.FocusManager)
572-
?.ClearFocusOnElementRemoved(this, root);
571+
(((FocusManager?)e.PresentationSource.InputRoot.FocusManager) ??
572+
FocusManager.GetFocusManager(this))?.ClearFocusOnElementRemoved(this, root);
573573
}
574574

575575
IsKeyboardFocusWithin = false;

src/Avalonia.Base/Media/GlyphRun.cs

Lines changed: 22 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -160,7 +160,10 @@ public double FontRenderingEmSize
160160
public Rect Bounds => new Rect(new Point(BaselineOrigin.X, 0),
161161
new Size(Metrics.WidthIncludingTrailingWhitespace, Metrics.Height));
162162

163-
public Rect InkBounds => PlatformImpl.Item.Bounds;
163+
/// <summary>
164+
/// Gets the conservative bounding box of the inked area of the <see cref="GlyphRun"/>.
165+
/// </summary>
166+
public Rect InkBounds => _glyphInfos.Count == 0 ? default : PlatformImpl.Item.Bounds;
164167

165168
/// <summary>
166169
///
@@ -246,6 +249,11 @@ public Geometry BuildGeometry()
246249
/// </returns>
247250
public double GetDistanceFromCharacterHit(CharacterHit characterHit)
248251
{
252+
if (_glyphInfos.Count == 0)
253+
{
254+
return 0;
255+
}
256+
249257
var characterIndex = characterHit.FirstCharacterIndex + characterHit.TrailingLength;
250258
var isTrailingHit = characterHit.TrailingLength > 0;
251259

@@ -473,6 +481,11 @@ public CharacterHit GetPreviousCaretCharacterHit(CharacterHit characterHit)
473481
/// </returns>
474482
public int FindGlyphIndex(int characterIndex)
475483
{
484+
if (_glyphInfos.Count == 0)
485+
{
486+
return 0;
487+
}
488+
476489
if (_hasOneCharPerCluster)
477490
{
478491
return characterIndex;
@@ -559,6 +572,14 @@ public CharacterHit FindNearestCharacterHit(int index, out double width)
559572
{
560573
width = 0.0;
561574

575+
// A run can hold characters and no glyphs at all - a line break in a font that gives the
576+
// shaper no way to hide it shapes to nothing. There is no cluster to snap to, and the
577+
// whole run sits at one position, so treat it as a single zero-width cluster.
578+
if (_glyphInfos.Count == 0)
579+
{
580+
return new CharacterHit(Metrics.FirstCluster, _characters.Length);
581+
}
582+
562583
var glyphIndex = FindGlyphIndex(index);
563584

564585
if (_hasOneCharPerCluster)

src/Avalonia.Base/Media/TextFormatting/TextFormatterImpl.cs

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -541,7 +541,12 @@ private static void ShapeTogether(IReadOnlyList<UnshapedTextRun> textRuns, ReadO
541541

542542
var splitResult = shapedBuffer.Split(previousLength + currentRun.Length);
543543

544-
if (splitResult.First is null || splitResult.First.Length == 0)
544+
// Split by text, not by glyph count: a run can legitimately shape to no glyphs at
545+
// all and still own its characters. Shapers drop default ignorables that the font
546+
// cannot hide behind a space glyph, so a run holding nothing but a line break can
547+
// come back empty. Skipping it there would delete its characters from the line and
548+
// leave the caller stuck at the same text position.
549+
if (splitResult.First is null || splitResult.First.Text.Length == 0)
545550
{
546551
previousLength += currentRun.Length;
547552
}

src/Avalonia.Base/Platform/PlatformGraphicsExternalMemory.cs

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,19 @@ public record struct PlatformGraphicsExternalImageProperties
88
public ulong MemorySize { get; set; }
99
public ulong MemoryOffset { get; set; }
1010
public bool TopLeftOrigin { get; set; }
11+
12+
/// <summary>
13+
/// Vulkan-specific properties of the imported image, ignored by other backends.
14+
/// </summary>
15+
public PlatformGraphicsExternalImageVulkanProperties? VulkanProperties { get; set; }
16+
}
17+
18+
public record struct PlatformGraphicsExternalImageVulkanProperties
19+
{
20+
/// <summary>
21+
/// The VkImageLayout the underlying memory is currently in.
22+
/// </summary>
23+
public int Layout { get; set; }
1124
}
1225

1326
public enum PlatformGraphicsExternalImageFormat

0 commit comments

Comments
 (0)