From c02bc4f183aa16ef9ea32bf0371ae3d65611296b Mon Sep 17 00:00:00 2001 From: lindexi Date: Tue, 21 Jan 2025 19:21:10 +0800 Subject: [PATCH 01/16] Add _NET_WM_PID atom to Linux X11 window (#17470) * Add `_NET_WM_PID` atom to Linux X11 window https://github.com/AvaloniaUI/Avalonia/issues/17444 * Also append WM_CLIENT_MACHINE * Get the host name from uname method to avoid read the file * Fix build fail on .NET Standard 2.0 * The render window is the child window. And it should not append pid atom. * Using the atom from atoms * Replace UtsName with gethostname --- src/Avalonia.X11/X11Window.cs | 54 +++++++++++++++++++++++++++++++++++ 1 file changed, 54 insertions(+) diff --git a/src/Avalonia.X11/X11Window.cs b/src/Avalonia.X11/X11Window.cs index ceb9c5edb8a..7fbeaa35cd9 100644 --- a/src/Avalonia.X11/X11Window.cs +++ b/src/Avalonia.X11/X11Window.cs @@ -168,15 +168,20 @@ public X11Window(AvaloniaX11Platform platform, IWindowImpl? popupParent, X11Wind (int)CreateWindowArgs.InputOutput, visual, new UIntPtr((uint)valueMask), ref attr); + AppendPid(_handle); if (_useRenderWindow) + { _renderHandle = XCreateWindow(_x11.Display, _handle, 0, 0, defaultWidth, defaultHeight, 0, depth, (int)CreateWindowArgs.InputOutput, visual, new UIntPtr((uint)(SetWindowValuemask.BorderPixel | SetWindowValuemask.BitGravity | SetWindowValuemask.WinGravity | SetWindowValuemask.BackingStore)), ref attr); + } else + { _renderHandle = _handle; + } Handle = new PlatformHandle(_handle, "XID"); @@ -335,6 +340,55 @@ private void UpdateMotifHints() PropertyMode.Replace, ref hints, 5); } + /// + /// Append `_NET_WM_PID` atom to X11 window + /// + /// + private void AppendPid(IntPtr windowXId) + { + // See https://github.com/AvaloniaUI/Avalonia/issues/17444 + var pid = (uint)s_pid; + // The type of `_NET_WM_PID` is `CARDINAL` which is 32-bit unsigned integer, see https://specifications.freedesktop.org/wm-spec/1.3/ar01s05.html + XChangeProperty(_x11.Display, windowXId, + _x11.Atoms._NET_WM_PID, _x11.Atoms.XA_CARDINAL, 32, + PropertyMode.Replace, ref pid, 1); + + const int maxLength = 1024; + var name = stackalloc byte[maxLength]; + var result = gethostname(name, maxLength); + if (result != 0) + { + // Fail + return; + } + + var length = 0; + while (length < maxLength && name[length] != 0) + { + length++; + } + + XChangeProperty(_x11.Display, windowXId, + _x11.Atoms.XA_WM_CLIENT_MACHINE, _x11.Atoms.XA_STRING, 8, + PropertyMode.Replace, name, length); + } + + [DllImport("libc")] + private static extern int gethostname(byte* name, int len); + + private static readonly int s_pid = GetProcessId(); + + private static int GetProcessId() + { +#if NET6_0_OR_GREATER + var pid = Environment.ProcessId; +#else + using var currentProcess = Process.GetCurrentProcess(); + var pid = currentProcess.Id; +#endif + return pid; + } + private void UpdateSizeHints(PixelSize? preResize, bool forceDisableResize = false) { if (_overrideRedirect) From b45872e728a8dd7de0fdaabd013ec7c1c0378dbc Mon Sep 17 00:00:00 2001 From: Joseph Sawyer Date: Wed, 22 Jan 2025 00:08:39 +0000 Subject: [PATCH 02/16] feat: add hidesuggestions property to textbox (#17815) * feat: add hidesuggestions property to textbox * fix: set HideSuggestions in FromStyledElement * refactor: api review recommendations * refactor: pr suggestions --- samples/ControlCatalog/Pages/TextBoxPage.xaml | 1 + .../Platform/Input/AndroidInputMethod.cs | 3 ++ .../Input/TextInput/TextInputOptions.cs | 36 ++++++++++++++++++- .../TextInputResponder.Properties.cs | 11 ++++-- 4 files changed, 48 insertions(+), 3 deletions(-) diff --git a/samples/ControlCatalog/Pages/TextBoxPage.xaml b/samples/ControlCatalog/Pages/TextBoxPage.xaml index 058bcff84de..e2230e3cb64 100644 --- a/samples/ControlCatalog/Pages/TextBoxPage.xaml +++ b/samples/ControlCatalog/Pages/TextBoxPage.xaml @@ -38,6 +38,7 @@ UseFloatingWatermark="True" PasswordChar="*" Text="Password" /> + diff --git a/src/Android/Avalonia.Android/Platform/Input/AndroidInputMethod.cs b/src/Android/Avalonia.Android/Platform/Input/AndroidInputMethod.cs index cb105197cbc..89e7dddb789 100644 --- a/src/Android/Avalonia.Android/Platform/Input/AndroidInputMethod.cs +++ b/src/Android/Avalonia.Android/Platform/Input/AndroidInputMethod.cs @@ -172,6 +172,9 @@ public void SetOptions(TextInputOptions options) if (options.Multiline) outAttrs.InputType |= InputTypes.TextFlagMultiLine; + if (outAttrs.InputType is InputTypes.ClassText && options.ShowSuggestions == false) + outAttrs.InputType |= InputTypes.TextVariationPassword | InputTypes.TextFlagNoSuggestions; + outAttrs.ImeOptions = options.ReturnKeyType switch { TextInputReturnKeyType.Return => ImeFlags.NoEnterAction, diff --git a/src/Avalonia.Base/Input/TextInput/TextInputOptions.cs b/src/Avalonia.Base/Input/TextInput/TextInputOptions.cs index c33f5641a85..deed705ef4b 100644 --- a/src/Avalonia.Base/Input/TextInput/TextInputOptions.cs +++ b/src/Avalonia.Base/Input/TextInput/TextInputOptions.cs @@ -12,7 +12,8 @@ public static TextInputOptions FromStyledElement(StyledElement avaloniaObject) AutoCapitalization = GetAutoCapitalization(avaloniaObject), IsSensitive = GetIsSensitive(avaloniaObject), Lowercase = GetLowercase(avaloniaObject), - Uppercase = GetUppercase(avaloniaObject) + Uppercase = GetUppercase(avaloniaObject), + ShowSuggestions = GetShowSuggestions(avaloniaObject), }; return result; @@ -253,4 +254,37 @@ public static bool GetIsSensitive(StyledElement avaloniaObject) /// Text contains sensitive data like card numbers and should not be stored /// public bool IsSensitive { get; set; } + + /// + /// Defines the property. + /// + public static readonly AttachedProperty ShowSuggestionsProperty = + AvaloniaProperty.RegisterAttached( + "ShowSuggestions", + inherits: true); + + /// + /// Sets the value of the attached on a control. + /// + /// The control. + /// The property value to set. + public static void SetShowSuggestions(StyledElement avaloniaObject, bool? value) + { + avaloniaObject.SetValue(ShowSuggestionsProperty, value); + } + + /// + /// Gets the value of the attached . + /// + /// The target. + /// true if ShowSuggestions + public static bool? GetShowSuggestions(StyledElement avaloniaObject) + { + return avaloniaObject.GetValue(ShowSuggestionsProperty); + } + + /// + /// Show virtual keyboard suggestions + /// + public bool? ShowSuggestions { get; set; } } diff --git a/src/iOS/Avalonia.iOS/TextInputResponder.Properties.cs b/src/iOS/Avalonia.iOS/TextInputResponder.Properties.cs index ab0d92e5fae..0614205a049 100644 --- a/src/iOS/Avalonia.iOS/TextInputResponder.Properties.cs +++ b/src/iOS/Avalonia.iOS/TextInputResponder.Properties.cs @@ -12,7 +12,10 @@ partial class TextInputResponder public UITextAutocapitalizationType AutocapitalizationType { get; private set; } [Export("autocorrectionType")] - public UITextAutocorrectionType AutocorrectionType => UITextAutocorrectionType.Yes; + public UITextAutocorrectionType AutocorrectionType => + _view._options?.ShowSuggestions == false ? + UITextAutocorrectionType.No : + UITextAutocorrectionType.Yes; [Export("keyboardType")] public UIKeyboardType KeyboardType => @@ -64,7 +67,11 @@ public UIReturnKeyType ReturnKeyType _view._options?.ContentType is TextInputContentType.Password or TextInputContentType.Pin || (_view._options?.IsSensitive ?? false); - [Export("spellCheckingType")] public UITextSpellCheckingType SpellCheckingType => UITextSpellCheckingType.Yes; + [Export("spellCheckingType")] + public UITextSpellCheckingType SpellCheckingType => + _view._options?.ShowSuggestions == false ? + UITextSpellCheckingType.No : + UITextSpellCheckingType.Yes; [Export("textContentType")] public NSString TextContentType { get; set; } = new NSString("text/plain"); From 3718c10408a4cd6f25437e0f97135664f05d95fe Mon Sep 17 00:00:00 2001 From: Yoshihiro Ito Date: Wed, 22 Jan 2025 14:45:44 +0900 Subject: [PATCH 03/16] Fix in HeadlessClipboardStub.GetDataAsync where format is not considered (#18008) --- .../Avalonia.Headless/HeadlessPlatformStubs.cs | 12 +++++++++++- 1 file changed, 11 insertions(+), 1 deletion(-) diff --git a/src/Headless/Avalonia.Headless/HeadlessPlatformStubs.cs b/src/Headless/Avalonia.Headless/HeadlessPlatformStubs.cs index 78fcb9ea744..e4842295512 100644 --- a/src/Headless/Avalonia.Headless/HeadlessPlatformStubs.cs +++ b/src/Headless/Avalonia.Headless/HeadlessPlatformStubs.cs @@ -66,7 +66,17 @@ public Task GetFormatsAsync() public async Task GetDataAsync(string format) { - return await Task.Run(() => _data); + return await Task.Run(() => + { + if (format == DataFormats.Text) + return _text; + if (format == DataFormats.Files && _data is not null) + return _data.GetFiles(); + if (format == DataFormats.FileNames && _data is not null) + return _data.GetFileNames(); + else + return (object?)_data; + }); } } From 26bc22910f66d698a04419e737b8892eb34d60fa Mon Sep 17 00:00:00 2001 From: Dong Bin <14807942+rabbitism@users.noreply.github.com> Date: Wed, 22 Jan 2025 17:13:20 +0800 Subject: [PATCH 04/16] [TextBox] Add readonly property: LineCount. (#17656) * feat: add readonly property: LineCount. * feat: change property to method. --- src/Avalonia.Controls/TextBox.cs | 14 +++ .../TextBoxTests.cs | 100 ++++++++++++++++++ 2 files changed, 114 insertions(+) diff --git a/src/Avalonia.Controls/TextBox.cs b/src/Avalonia.Controls/TextBox.cs index b19c2eab273..39323d518ba 100644 --- a/src/Avalonia.Controls/TextBox.cs +++ b/src/Avalonia.Controls/TextBox.cs @@ -823,6 +823,20 @@ public bool CanRedo private set => SetAndRaise(CanRedoProperty, ref _canRedo, value); } + /// + /// Get the number of lines in the TextBox. + /// + /// number of lines in the TextBox, or -1 if no layout information is available + /// + /// If Wrap == true, changing the width of the TextBox may change this value. + /// The value returned is the number of lines in the entire TextBox, regardless of how many are + /// currently in view. + /// + public int GetLineCount() + { + return this._presenter?.TextLayout.TextLines.Count ?? -1; + } + /// /// Raised when content is being copied to the clipboard /// diff --git a/tests/Avalonia.Controls.UnitTests/TextBoxTests.cs b/tests/Avalonia.Controls.UnitTests/TextBoxTests.cs index 7aacaca1748..e85aa99fc9f 100644 --- a/tests/Avalonia.Controls.UnitTests/TextBoxTests.cs +++ b/tests/Avalonia.Controls.UnitTests/TextBoxTests.cs @@ -1235,6 +1235,106 @@ public void MinLines_Sets_ScrollViewer_MinHeight_With_TextPresenter_Margin(int m Assert.Equal((minLines * target.LineHeight) + textPresenterMargin.Top + textPresenterMargin.Bottom, scrollViewer.MinHeight); } } + + [Theory] + [InlineData(null, 1)] + [InlineData("", 1)] + [InlineData("Hello", 1)] + [InlineData("Hello\r\nWorld", 2)] + public void LineCount_Is_Correct(string? text, int lineCount) + { + using (UnitTestApplication.Start(Services)) + { + var target = new TextBox + { + Template = CreateTemplate(), + Text = text, + AcceptsReturn = true + }; + + var impl = CreateMockTopLevelImpl(); + var topLevel = new TestTopLevel(impl.Object) + { + Template = CreateTopLevelTemplate() + }; + topLevel.Content = target; + topLevel.ApplyTemplate(); + topLevel.LayoutManager.ExecuteInitialLayoutPass(); + + target.ApplyTemplate(); + target.Measure(Size.Infinity); + + Assert.Equal(lineCount, target.GetLineCount()); + } + } + + [Fact] + public void Unmeasured_TextBox_Has_Negative_LineCount() + { + var b = new TextBox(); + Assert.Equal(-1, b.GetLineCount()); + } + + [Fact] + public void LineCount_Is_Correct_After_Text_Change() + { + using (UnitTestApplication.Start(Services)) + { + var target = new TextBox + { + Template = CreateTemplate(), + Text = "Hello", + AcceptsReturn = true + }; + + var impl = CreateMockTopLevelImpl(); + var topLevel = new TestTopLevel(impl.Object) + { + Template = CreateTopLevelTemplate() + }; + topLevel.Content = target; + topLevel.ApplyTemplate(); + topLevel.LayoutManager.ExecuteInitialLayoutPass(); + + target.ApplyTemplate(); + target.Measure(Size.Infinity); + + Assert.Equal(1, target.GetLineCount()); + + target.Text = "Hello\r\nWorld"; + + Assert.Equal(2, target.GetLineCount()); + } + } + + [Fact] + public void Visible_LineCount_DoesNot_Affect_LineCount() + { + using (UnitTestApplication.Start(Services)) + { + var target = new TextBox + { + Template = CreateTemplate(), + Text = "Hello\r\nWorld\r\nHello\r\nAvalonia", + AcceptsReturn = true, + MaxLines = 2, + }; + + var impl = CreateMockTopLevelImpl(); + var topLevel = new TestTopLevel(impl.Object) + { + Template = CreateTopLevelTemplate() + }; + topLevel.Content = target; + topLevel.ApplyTemplate(); + topLevel.LayoutManager.ExecuteInitialLayoutPass(); + + target.ApplyTemplate(); + target.Measure(Size.Infinity); + + Assert.Equal(4, target.GetLineCount()); + } + } [Fact] public void CanUndo_CanRedo_Is_False_When_Initialized() From fdadfb862f830b6f74d7d3649b7088cb70fd8caf Mon Sep 17 00:00:00 2001 From: Mohamad Iraji Date: Wed, 22 Jan 2025 10:20:33 +0100 Subject: [PATCH 05/16] add spacing to uniform grid - issue 8406 (#17993) * add spacing to uniform grid - issue 8406 * fix negative return value from UniformGrid MeasureOverride and cached spacing properties --- .../Primitives/UniformGrid.cs | 59 +++++- .../Primitives/UniformGridTests.cs | 200 ++++++++++++++++-- 2 files changed, 235 insertions(+), 24 deletions(-) diff --git a/src/Avalonia.Controls/Primitives/UniformGrid.cs b/src/Avalonia.Controls/Primitives/UniformGrid.cs index fea35d867a3..f21cb20641e 100644 --- a/src/Avalonia.Controls/Primitives/UniformGrid.cs +++ b/src/Avalonia.Controls/Primitives/UniformGrid.cs @@ -25,6 +25,18 @@ public class UniformGrid : Panel public static readonly StyledProperty FirstColumnProperty = AvaloniaProperty.Register(nameof(FirstColumn)); + /// + /// Defines the property. + /// + public static readonly StyledProperty RowSpacingProperty = + AvaloniaProperty.Register(nameof(RowSpacing), 0); + + /// + /// Defines the property. + /// + public static readonly StyledProperty ColumnSpacingProperty = + AvaloniaProperty.Register(nameof(ColumnSpacing), 0); + private int _rows; private int _columns; @@ -60,6 +72,24 @@ public int FirstColumn set => SetValue(FirstColumnProperty, value); } + /// + /// Specifies the spacing between rows. + /// + public double RowSpacing + { + get => GetValue(RowSpacingProperty); + set => SetValue(RowSpacingProperty, value); + } + + /// + /// Specifies the spacing between columns. + /// + public double ColumnSpacing + { + get => GetValue(ColumnSpacingProperty); + set => SetValue(ColumnSpacingProperty, value); + } + protected override Size MeasureOverride(Size availableSize) { UpdateRowsAndColumns(); @@ -67,7 +97,9 @@ protected override Size MeasureOverride(Size availableSize) var maxWidth = 0d; var maxHeight = 0d; - var childAvailableSize = new Size(availableSize.Width / _columns, availableSize.Height / _rows); + var childAvailableSize = new Size( + (availableSize.Width - (_columns - 1) * ColumnSpacing) / _columns, + (availableSize.Height - (_rows - 1) * RowSpacing) / _rows); foreach (var child in Children) { @@ -84,7 +116,13 @@ protected override Size MeasureOverride(Size availableSize) } } - return new Size(maxWidth * _columns, maxHeight * _rows); + var totalWidth = maxWidth * _columns + ColumnSpacing * (_columns - 1); + var totalHeight = maxHeight * _rows + RowSpacing * (_rows - 1); + + totalWidth = Math.Max(totalWidth, 0); + totalHeight = Math.Max(totalHeight, 0); + + return new Size(totalWidth, totalHeight); } protected override Size ArrangeOverride(Size finalSize) @@ -92,8 +130,11 @@ protected override Size ArrangeOverride(Size finalSize) var x = FirstColumn; var y = 0; - var width = finalSize.Width / _columns; - var height = finalSize.Height / _rows; + var columnSpacing = ColumnSpacing; + var rowSpacing = RowSpacing; + + var width = (finalSize.Width - (_columns - 1) * columnSpacing) / _columns; + var height = (finalSize.Height - (_rows - 1) * rowSpacing) / _rows; foreach (var child in Children) { @@ -102,7 +143,13 @@ protected override Size ArrangeOverride(Size finalSize) continue; } - child.Arrange(new Rect(x * width, y * height, width, height)); + var rect = new Rect( + x * (width + columnSpacing), + y * (height + rowSpacing), + width, + height); + + child.Arrange(rect); x++; @@ -121,7 +168,7 @@ private void UpdateRowsAndColumns() _rows = Rows; _columns = Columns; - if (FirstColumn >= Columns) + if (FirstColumn >= _columns) { SetCurrentValue(FirstColumnProperty, 0); } diff --git a/tests/Avalonia.Controls.UnitTests/Primitives/UniformGridTests.cs b/tests/Avalonia.Controls.UnitTests/Primitives/UniformGridTests.cs index 340bd09611e..d5cc450587f 100644 --- a/tests/Avalonia.Controls.UnitTests/Primitives/UniformGridTests.cs +++ b/tests/Avalonia.Controls.UnitTests/Primitives/UniformGridTests.cs @@ -8,7 +8,7 @@ public class UniformGridTests [Fact] public void Grid_Columns_Equals_Rows_For_Auto_Columns_And_Rows() { - var target = new UniformGrid() + var target = new UniformGrid { Children = { @@ -21,14 +21,15 @@ public void Grid_Columns_Equals_Rows_For_Auto_Columns_And_Rows() target.Measure(Size.Infinity); target.Arrange(new Rect(target.DesiredSize)); - // 2 * 2 grid - Assert.Equal(new Size(2 * 80, 2 * 90), target.Bounds.Size); + // 2 * 2 grid => each cell: 80 x 90 + // Final size => (2 * 80) x (2 * 90) = 160 x 180 + Assert.Equal(new Size(160, 180), target.Bounds.Size); } [Fact] public void Grid_Expands_Vertically_For_Columns_With_Auto_Rows() { - var target = new UniformGrid() + var target = new UniformGrid { Columns = 2, Children = @@ -44,14 +45,15 @@ public void Grid_Expands_Vertically_For_Columns_With_Auto_Rows() target.Measure(Size.Infinity); target.Arrange(new Rect(target.DesiredSize)); - // 2 * 3 grid - Assert.Equal(new Size(2 * 80, 3 * 90), target.Bounds.Size); + // 2 * 3 grid => each cell: 80 x 90 + // Final size => (2 * 80) x (3 * 90) = 160 x 270 + Assert.Equal(new Size(160, 270), target.Bounds.Size); } [Fact] public void Grid_Extends_For_Columns_And_First_Column_With_Auto_Rows() { - var target = new UniformGrid() + var target = new UniformGrid { Columns = 3, FirstColumn = 2, @@ -68,14 +70,15 @@ public void Grid_Extends_For_Columns_And_First_Column_With_Auto_Rows() target.Measure(Size.Infinity); target.Arrange(new Rect(target.DesiredSize)); - // 3 * 3 grid - Assert.Equal(new Size(3 * 80, 3 * 90), target.Bounds.Size); + // 3 * 3 grid => each cell: 80 x 90 + // Final size => (3 * 80) x (3 * 90) = 240 x 270 + Assert.Equal(new Size(240, 270), target.Bounds.Size); } [Fact] public void Grid_Expands_Horizontally_For_Rows_With_Auto_Columns() { - var target = new UniformGrid() + var target = new UniformGrid { Rows = 2, Children = @@ -91,14 +94,15 @@ public void Grid_Expands_Horizontally_For_Rows_With_Auto_Columns() target.Measure(Size.Infinity); target.Arrange(new Rect(target.DesiredSize)); - // 3 * 2 grid - Assert.Equal(new Size(3 * 80, 2 * 90), target.Bounds.Size); + // 3 * 2 grid => each cell: 80 x 90 + // Final size => (3 * 80) x (2 * 90) = 240 x 180 + Assert.Equal(new Size(240, 180), target.Bounds.Size); } [Fact] public void Grid_Size_Is_Limited_By_Rows_And_Columns() { - var target = new UniformGrid() + var target = new UniformGrid { Columns = 2, Rows = 2, @@ -115,14 +119,15 @@ public void Grid_Size_Is_Limited_By_Rows_And_Columns() target.Measure(Size.Infinity); target.Arrange(new Rect(target.DesiredSize)); - // 2 * 2 grid - Assert.Equal(new Size(2 * 80, 2 * 90), target.Bounds.Size); + // 2 * 2 grid => each cell: 80 x 90 + // Final size => (2 * 80) x (2 * 90) = 160 x 180 + Assert.Equal(new Size(160, 180), target.Bounds.Size); } [Fact] public void Not_Visible_Children_Are_Ignored() { - var target = new UniformGrid() + var target = new UniformGrid { Children = { @@ -137,8 +142,167 @@ public void Not_Visible_Children_Are_Ignored() target.Measure(Size.Infinity); target.Arrange(new Rect(target.DesiredSize)); - // 2 * 2 grid - Assert.Equal(new Size(2 * 50, 2 * 70), target.Bounds.Size); + // Visible children: 4 + // Auto => 2 x 2 grid => each cell: 50 x 70 + // Final size => (2 * 50) x (2 * 70) = 100 x 140 + Assert.Equal(new Size(100, 140), target.Bounds.Size); + } + + // + // New tests to cover RowSpacing and ColumnSpacing + // + + [Fact] + public void Grid_Respects_ColumnSpacing_For_Auto_Columns_And_Rows() + { + // We have 3 visible children and no fixed Rows/Columns => 2x2 grid + // Largest child is 80 x 90. ColumnSpacing = 10, RowSpacing = 0 + var target = new UniformGrid + { + ColumnSpacing = 10, + Children = + { + new Border { Width = 50, Height = 70 }, + new Border { Width = 30, Height = 50 }, + new Border { Width = 80, Height = 90 } + } + }; + + target.Measure(Size.Infinity); + target.Arrange(new Rect(target.DesiredSize)); + + // Without spacing => width = 2*80 = 160, height = 2*90 = 180 + // With columnSpacing=10 => total width = 2*80 + 1*10 = 170 + // RowSpacing=0 => total height = 180 + Assert.Equal(new Size(170, 180), target.Bounds.Size); + } + + [Fact] + public void Grid_Respects_RowSpacing_For_Auto_Columns_And_Rows() + { + // 3 visible children => 2x2 grid again + // Largest child is 80 x 90. RowSpacing = 15, ColumnSpacing = 0 + var target = new UniformGrid + { + RowSpacing = 15, + Children = + { + new Border { Width = 50, Height = 70 }, + new Border { Width = 30, Height = 50 }, + new Border { Width = 80, Height = 90 } + } + }; + + target.Measure(Size.Infinity); + target.Arrange(new Rect(target.DesiredSize)); + + // Without spacing => width = 160, height = 180 + // With rowSpacing=15 => total height = 2*90 + 1*15 = 195 + // ColumnSpacing=0 => total width = 160 + Assert.Equal(new Size(160, 195), target.Bounds.Size); + } + + [Fact] + public void Grid_Respects_Both_Row_And_Column_Spacing_For_Fixed_Grid() + { + // 4 visible children => 2 rows x 2 columns, each child is 50x70 or 80x90 + // We'll fix the Grid to 2x2 so the largest child dictates the cell size: 80x90 + // RowSpacing=10, ColumnSpacing=5 + var target = new UniformGrid + { + Rows = 2, + Columns = 2, + RowSpacing = 10, + ColumnSpacing = 5, + Children = + { + new Border { Width = 50, Height = 70 }, + new Border { Width = 30, Height = 50 }, + new Border { Width = 80, Height = 90 }, + new Border { Width = 20, Height = 30 }, + } + }; + + target.Measure(Size.Infinity); + target.Arrange(new Rect(target.DesiredSize)); + + // Each cell = 80 x 90 + // Final width = (2 * 80) + (1 * 5) = 160 + 5 = 165 + // Final height = (2 * 90) + (1 * 10) = 180 + 10 = 190 + Assert.Equal(new Size(165, 190), target.Bounds.Size); + } + + [Fact] + public void Grid_Respects_Spacing_When_Invisible_Child_Exists() + { + // 3 *visible* children => auto => 2x2 grid + // Largest child is 80 x 90. + // Add spacing so we can confirm it doesn't add extra columns/rows for invisible child. + var target = new UniformGrid + { + RowSpacing = 5, + ColumnSpacing = 5, + Children = + { + new Border { Width = 50, Height = 70 }, + new Border { Width = 80, Height = 90, IsVisible = false }, + new Border { Width = 30, Height = 50 }, + new Border { Width = 40, Height = 60 } + } + }; + + // Visible children: 3 => auto => sqrt(3) => 2x2 + // Largest visible child is 50x70 or 30x50 or 40x60 => the biggest is 50x70 + // Actually, let's ensure we have a child bigger than that: + // (So let's modify the 40x60 to something bigger than 50x70, e.g. 80x90 for clarity) + // We'll do that in the collection above if needed, but let's keep as is for example. + + target.Measure(Size.Infinity); + target.Arrange(new Rect(target.DesiredSize)); + + // The largest visible child is 50x70. So each cell is 50x70. + // For a 2x2 grid with 3 visible children: + // - total width = (2 * 50) + (1 * 5) = 100 + 5 = 105 + // - total height = (2 * 70) + (1 * 5) = 140 + 5 = 145 + Assert.Equal(new Size(105, 145), target.Bounds.Size); + } + + /// + /// Exposes MeasureOverride for testing inherited classes + /// + public class UniformGridExposeMeasureOverride : UniformGrid + { + public new Size MeasureOverride(Size availableSize) + { + return base.MeasureOverride(availableSize); + } + } + + [Fact] + public void Measure_WithRowsAndColumnsZeroAndNonZeroSpacing_ProducesZeroDesiredSize() + { + // MeasureOverride() is called by Layoutable.MeasureCore() and it ensures that + // the desired size is never negative. but in case of inherited classes MeasureOverride() may return negative values. + var target = new UniformGridExposeMeasureOverride + { + Rows = 0, + Columns = 0, + RowSpacing = 10, + ColumnSpacing = 20 + }; + + var availableSize = new Size(100, 100); + + var desiredSize = target.MeasureOverride(availableSize); + + // Fail case: + // Because _rows and _columns are 0, the calculation becomes: + // totalWidth = maxWidth * 0 + ColumnSpacing * (0 - 1) = -ColumnSpacing + // totalHeight = maxHeight * 0 + RowSpacing * (0 - 1) = -RowSpacing + // Expected: (0, 0) + Assert.Equal(0, desiredSize.Width); + Assert.Equal(0, desiredSize.Height); + } } } From c15dc0d8c04310e5697c20fe6a7bc232bc28fcb5 Mon Sep 17 00:00:00 2001 From: kerams Date: Wed, 22 Jan 2025 22:21:49 +0100 Subject: [PATCH 06/16] Support activation with universal links (#18005) --- src/iOS/Avalonia.iOS/AvaloniaAppDelegate.cs | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/src/iOS/Avalonia.iOS/AvaloniaAppDelegate.cs b/src/iOS/Avalonia.iOS/AvaloniaAppDelegate.cs index 99e97a56315..427a9e52eba 100644 --- a/src/iOS/Avalonia.iOS/AvaloniaAppDelegate.cs +++ b/src/iOS/Avalonia.iOS/AvaloniaAppDelegate.cs @@ -89,6 +89,19 @@ public bool OpenUrl(UIApplication app, NSUrl url, NSDictionary options) return false; } + [Export("application:continueUserActivity:restorationHandler:")] + public bool ContinueUserActivity(UIApplication application, NSUserActivity userActivity, UIApplicationRestorationHandler completionHandler) + { + if (userActivity.ActivityType == NSUserActivityType.BrowsingWeb && Uri.TryCreate(userActivity.WebPageUrl?.ToString(), UriKind.RelativeOrAbsolute, out var uri)) + { + // Activation using a univeral link or web browser-to-native app Handoff + _onActivated?.Invoke(this, new ProtocolActivatedEventArgs(uri)); + return true; + } + + return false; + } + private void OnEnteredBackground(NSNotification notification) { _onDeactivated?.Invoke(this, new ActivatedEventArgs(ActivationKind.Background)); From 3850b80e1c7a5720d66c6f4bfa0a10f38ae54feb Mon Sep 17 00:00:00 2001 From: Maxwell Katz Date: Wed, 22 Jan 2025 15:10:12 -0800 Subject: [PATCH 07/16] Bump Avalonia.BuildServices (#18029) --- packages/Avalonia/Avalonia.csproj | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/Avalonia/Avalonia.csproj b/packages/Avalonia/Avalonia.csproj index daf5c47f60a..b954f02f47e 100644 --- a/packages/Avalonia/Avalonia.csproj +++ b/packages/Avalonia/Avalonia.csproj @@ -5,7 +5,7 @@ - + From fce56827e2e3bfd700850fe3449dd4b82f364275 Mon Sep 17 00:00:00 2001 From: nickodei <46863421+nickodei@users.noreply.github.com> Date: Thu, 23 Jan 2025 16:07:12 +0100 Subject: [PATCH 08/16] Access single file or folder from IStorageFolder by name (#17771) * added methods to IStorageFolder to access a single file or folder by name * relaxed file/folder by name browser acces to throw less errors --- api/Avalonia.nupkg.xml | 12 +++ .../Platform/Storage/AndroidStorageItem.cs | 73 +++++++++++++++++++ .../Storage/FileIO/BclStorageFolder.cs | 6 ++ .../Platform/Storage/FileIO/BclStorageItem.cs | 22 ++++++ .../Platform/Storage/IStorageFolder.cs | 18 +++++ src/Avalonia.Native/StorageItem.cs | 14 ++++ .../Avalonia.Browser/Interop/StorageHelper.cs | 6 ++ .../Storage/BrowserStorageProvider.cs | 43 +++++++++++ .../webapp/modules/storage/storageItem.ts | 20 +++++ .../Avalonia.iOS/Storage/IOSStorageItem.cs | 32 ++++++++ 10 files changed, 246 insertions(+) diff --git a/api/Avalonia.nupkg.xml b/api/Avalonia.nupkg.xml index f6797ada377..6b0f1c59cc4 100644 --- a/api/Avalonia.nupkg.xml +++ b/api/Avalonia.nupkg.xml @@ -49,6 +49,18 @@ baseline/netstandard2.0/Avalonia.Controls.dll target/netstandard2.0/Avalonia.Controls.dll + + CP0006 + M:Avalonia.Platform.Storage.IStorageFolder.GetFileAsync(System.String) + baseline/netstandard2.0/Avalonia.Base.dll + target/netstandard2.0/Avalonia.Base.dll + + + CP0006 + M:Avalonia.Platform.Storage.IStorageFolder.GetFolderAsync(System.String) + baseline/netstandard2.0/Avalonia.Base.dll + target/netstandard2.0/Avalonia.Base.dll + CP0006 M:Avalonia.Controls.Notifications.IManagedNotificationManager.Close(System.Object) diff --git a/src/Android/Avalonia.Android/Platform/Storage/AndroidStorageItem.cs b/src/Android/Avalonia.Android/Platform/Storage/AndroidStorageItem.cs index bb27379a702..330f2ded867 100644 --- a/src/Android/Avalonia.Android/Platform/Storage/AndroidStorageItem.cs +++ b/src/Android/Avalonia.Android/Platform/Storage/AndroidStorageItem.cs @@ -294,6 +294,79 @@ public async IAsyncEnumerable GetItemsAsync() return destination; } } + + private async Task GetItemAsync(string name, bool isDirectory) + { + if (!await EnsureExternalFilesPermission(false)) + { + return null; + } + + var contentResolver = Activity.ContentResolver; + if (contentResolver == null) + { + return null; + } + + var root = PermissionRoot ?? Uri; + var folderId = root != Uri ? DocumentsContract.GetDocumentId(Uri) : DocumentsContract.GetTreeDocumentId(Uri); + var childrenUri = DocumentsContract.BuildChildDocumentsUriUsingTree(root, folderId); + + var projection = new[] + { + DocumentsContract.Document.ColumnDocumentId, + DocumentsContract.Document.ColumnMimeType, + DocumentsContract.Document.ColumnDisplayName + }; + + if (childrenUri != null) + { + using var cursor = contentResolver.Query(childrenUri, projection, null, null, null); + if (cursor != null) + { + while (cursor.MoveToNext()) + { + var id = cursor.GetString(0); + var mime = cursor.GetString(1); + + var fileName = cursor.GetString(2); + if (fileName != name) + { + continue; + } + + bool mineDirectory = mime == DocumentsContract.Document.MimeTypeDir; + if (isDirectory != mineDirectory) + { + return null; + } + + var uri = DocumentsContract.BuildDocumentUriUsingTree(root, id); + if (uri == null) + { + return null; + } + + return isDirectory ? new AndroidStorageFolder(Activity, uri, false, this, root) : + new AndroidStorageFile(Activity, uri, this, root); + } + } + } + + return null; + } + + public async Task GetFolderAsync(string name) + { + var folder = await GetItemAsync(name, true); + return (IStorageFolder?)folder; + } + + public async Task GetFileAsync(string name) + { + var file = await GetItemAsync(name, false); + return (IStorageFile?)file; + } } internal sealed class WellKnownAndroidStorageFolder : AndroidStorageFolder diff --git a/src/Avalonia.Base/Platform/Storage/FileIO/BclStorageFolder.cs b/src/Avalonia.Base/Platform/Storage/FileIO/BclStorageFolder.cs index 05572d60586..73f75355d6c 100644 --- a/src/Avalonia.Base/Platform/Storage/FileIO/BclStorageFolder.cs +++ b/src/Avalonia.Base/Platform/Storage/FileIO/BclStorageFolder.cs @@ -19,4 +19,10 @@ public IAsyncEnumerable GetItemsAsync() => GetItemsCore(directoryI public Task CreateFolderAsync(string name) => Task.FromResult( (IStorageFolder?)WrapFileSystemInfo(CreateFolderCore(directoryInfo, name))); + + public Task GetFolderAsync(string name) => Task.FromResult( + (IStorageFolder?)WrapFileSystemInfo(GetFolderCore(directoryInfo, name))); + + public Task GetFileAsync(string name) => Task.FromResult( + (IStorageFile?)WrapFileSystemInfo(GetFileCore(directoryInfo, name))); } diff --git a/src/Avalonia.Base/Platform/Storage/FileIO/BclStorageItem.cs b/src/Avalonia.Base/Platform/Storage/FileIO/BclStorageItem.cs index 123d0e9283d..294fc2c5790 100644 --- a/src/Avalonia.Base/Platform/Storage/FileIO/BclStorageItem.cs +++ b/src/Avalonia.Base/Platform/Storage/FileIO/BclStorageItem.cs @@ -127,6 +127,28 @@ internal static IEnumerable GetItemsCore(DirectoryInfo directory .OfType() .Concat(directoryInfo.EnumerateFiles()); + internal static FileSystemInfo? GetFolderCore(DirectoryInfo directoryInfo, string name) + { + var path = System.IO.Path.Combine(directoryInfo.FullName, name); + if (Directory.Exists(path)) + { + return new DirectoryInfo(path); + } + + return null; + } + + internal static FileSystemInfo? GetFileCore(DirectoryInfo directoryInfo, string name) + { + var path = System.IO.Path.Combine(directoryInfo.FullName, name); + if (File.Exists(path)) + { + return new FileInfo(path); + } + + return null; + } + internal static FileInfo CreateFileCore(DirectoryInfo directoryInfo, string name) { var fileName = System.IO.Path.Combine(directoryInfo.FullName, name); diff --git a/src/Avalonia.Base/Platform/Storage/IStorageFolder.cs b/src/Avalonia.Base/Platform/Storage/IStorageFolder.cs index a9d1ff36692..b6d8fd4dafb 100644 --- a/src/Avalonia.Base/Platform/Storage/IStorageFolder.cs +++ b/src/Avalonia.Base/Platform/Storage/IStorageFolder.cs @@ -18,6 +18,24 @@ public interface IStorageFolder : IStorageItem /// IAsyncEnumerable GetItemsAsync(); + /// + /// Gets the folder with the specified name from the current folder. + /// + /// The name of the folder to get + /// + /// When this method completes successfully, it returns the folder with the specified name from the current folder. + /// + Task GetFolderAsync(string name); + + /// + /// Gets the file with the specified name from the current folder. + /// + /// The name of the file to get + /// + /// When this method completes successfully, it returns the file with the specified name from the current folder. + /// + Task GetFileAsync(string name); + /// /// Creates a file with specified name as a child of the current storage folder /// diff --git a/src/Avalonia.Native/StorageItem.cs b/src/Avalonia.Native/StorageItem.cs index 12e4cc0a5e4..efa0404a340 100644 --- a/src/Avalonia.Native/StorageItem.cs +++ b/src/Avalonia.Native/StorageItem.cs @@ -149,4 +149,18 @@ IEnumerable GetItems() var folder = BclStorageItem.CreateFolderCore(directoryInfo, name); return Task.FromResult((IStorageFolder?)WrapFileSystemInfo(folder, ScopeOwnerUri)); } + + public Task GetFolderAsync(string name) + { + using var scope = OpenScope(); + var item = BclStorageItem.GetFolderCore(directoryInfo, name); + return Task.FromResult((IStorageFolder?)WrapFileSystemInfo(item, ScopeOwnerUri)); + } + + public Task GetFileAsync(string name) + { + using var scope = OpenScope(); + var item = BclStorageItem.GetFileCore(directoryInfo, name); + return Task.FromResult((IStorageFile?)WrapFileSystemInfo(item, ScopeOwnerUri)); + } } diff --git a/src/Browser/Avalonia.Browser/Interop/StorageHelper.cs b/src/Browser/Avalonia.Browser/Interop/StorageHelper.cs index c28efbb3086..76f1273f2e7 100644 --- a/src/Browser/Avalonia.Browser/Interop/StorageHelper.cs +++ b/src/Browser/Avalonia.Browser/Interop/StorageHelper.cs @@ -67,4 +67,10 @@ internal static partial class StorageHelper [JSImport("StorageProvider.createFolder", AvaloniaModule.StorageModuleName)] public static partial Task CreateFolder(JSObject folderHandle, string name); + + [JSImport("StorageItem.getFile", AvaloniaModule.StorageModuleName)] + public static partial Task GetFile(JSObject folderHandle, string name); + + [JSImport("StorageItem.getFolder", AvaloniaModule.StorageModuleName)] + public static partial Task GetFolder(JSObject folderHandle, string name); } diff --git a/src/Browser/Avalonia.Browser/Storage/BrowserStorageProvider.cs b/src/Browser/Avalonia.Browser/Storage/BrowserStorageProvider.cs index 6a89a13e023..cef13b4d3bd 100644 --- a/src/Browser/Avalonia.Browser/Storage/BrowserStorageProvider.cs +++ b/src/Browser/Avalonia.Browser/Storage/BrowserStorageProvider.cs @@ -16,6 +16,8 @@ internal class BrowserStorageProvider : IStorageProvider internal static ReadOnlySpan BrowserBookmarkKey => "browser"u8; internal const string PickerCancelMessage = "The user aborted a request"; internal const string NoPermissionsMessage = "Permissions denied"; + internal const string FileFolderNotFoundMessage = "A requested file or directory could not be found"; + internal const string TypeMissmatchMessage = "The path supplied exists, but was not an entry of requested type"; public bool CanOpen => true; public bool CanSave => true; @@ -385,4 +387,45 @@ public async IAsyncEnumerable GetItemsAsync() throw new UnauthorizedAccessException("User denied permissions to open the file", ex); } } + + public async Task GetFolderAsync(string name) + { + try + { + var storageFile = await StorageHelper.GetFolder(FileHandle, name); + if (storageFile is null) + { + return null; + } + + return new JSStorageFolder(storageFile); + } + catch (JSException ex) when (ShouldSupressErrorOnFileAccess(ex)) + { + return null; + } + } + + public async Task GetFileAsync(string name) + { + try + { + var storageFile = await StorageHelper.GetFile(FileHandle, name); + if (storageFile is null) + { + return null; + } + + return new JSStorageFile(storageFile); + } + catch (JSException ex) when (ShouldSupressErrorOnFileAccess(ex)) + { + return null; + } + } + + private static bool ShouldSupressErrorOnFileAccess(JSException ex) => + ex.Message == BrowserStorageProvider.NoPermissionsMessage || + ex.Message.Contains(BrowserStorageProvider.TypeMissmatchMessage, StringComparison.Ordinal) || + ex.Message.Contains(BrowserStorageProvider.FileFolderNotFoundMessage, StringComparison.Ordinal); } diff --git a/src/Browser/Avalonia.Browser/webapp/modules/storage/storageItem.ts b/src/Browser/Avalonia.Browser/webapp/modules/storage/storageItem.ts index 56833e448c9..f30e6f59161 100644 --- a/src/Browser/Avalonia.Browser/webapp/modules/storage/storageItem.ts +++ b/src/Browser/Avalonia.Browser/webapp/modules/storage/storageItem.ts @@ -107,6 +107,16 @@ export class StorageItem { return await ((item.handle as any).getFileHandle(name, { create: true }) as Promise); } + public static async getFile(item: StorageItem, name: string): Promise { + if (item.kind !== "directory" || !item.handle) { + return null; + } + + await item.verityPermissions("read"); + + return await ((item.handle as any).getFileHandle(name) as Promise); + } + public static async createFolder(item: StorageItem, name: string): Promise { if (item.kind !== "directory" || !item.handle) { throw new TypeError("Unable to create item in the requested directory"); @@ -117,6 +127,16 @@ export class StorageItem { return await ((item.handle as any).getDirectoryHandle(name, { create: true }) as Promise); } + public static async getFolder(item: StorageItem, name: string): Promise { + if (item.kind !== "directory" || !item.handle) { + return null; + } + + await item.verityPermissions("read"); + + return await ((item.handle as any).getDirectoryHandle(name) as Promise); + } + public static async deleteAsync(item: StorageItem): Promise { if (!item.handle) { return null; diff --git a/src/iOS/Avalonia.iOS/Storage/IOSStorageItem.cs b/src/iOS/Avalonia.iOS/Storage/IOSStorageItem.cs index fa086c7d61f..069b1e8d494 100644 --- a/src/iOS/Avalonia.iOS/Storage/IOSStorageItem.cs +++ b/src/iOS/Avalonia.iOS/Storage/IOSStorageItem.cs @@ -306,4 +306,36 @@ public async IAsyncEnumerable GetItemsAsync() SecurityScopedAncestorUrl.StopAccessingSecurityScopedResource(); } } + + private NSUrl? GetItem(string name, bool isDirectory) + { + try + { + SecurityScopedAncestorUrl.StartAccessingSecurityScopedResource(); + + var path = System.IO.Path.Combine(FilePath, name); + if (NSFileManager.DefaultManager.FileExists(path, ref isDirectory)) + { + return new NSUrl(path, isDirectory); + } + + return null; + } + finally + { + SecurityScopedAncestorUrl.StopAccessingSecurityScopedResource(); + } + } + + public Task GetFolderAsync(string name) + { + var url = GetItem(name, true); + return Task.FromResult(url is null ? null : new IOSStorageFolder(url)); + } + + public Task GetFileAsync(string name) + { + var url = GetItem(name, false); + return Task.FromResult(url is null ? null : new IOSStorageFile(url)); + } } From 58dacb051c75d713692575106587c1f661cf9e57 Mon Sep 17 00:00:00 2001 From: Yaroslav <116111680+IoannTerrible@users.noreply.github.com> Date: Thu, 23 Jan 2025 10:07:29 -0500 Subject: [PATCH 09/16] Add new constructor to AvaloniaDictionary and include unit tests #17311 (#17312) * Give AvaloniaDictionary new .ctor and add Tests for it #17311 * AvaloniaDictionary accept IDictionary as init collection --------- Co-authored-by: Julien Lebosquain --- .../Collections/AvaloniaDictionary.cs | 15 +++++++++ .../Collections/AvaloniaDictionaryTests.cs | 31 +++++++++++++++++++ 2 files changed, 46 insertions(+) diff --git a/src/Avalonia.Base/Collections/AvaloniaDictionary.cs b/src/Avalonia.Base/Collections/AvaloniaDictionary.cs index d4c7137fdcc..2fef8f77c88 100644 --- a/src/Avalonia.Base/Collections/AvaloniaDictionary.cs +++ b/src/Avalonia.Base/Collections/AvaloniaDictionary.cs @@ -34,6 +34,21 @@ public AvaloniaDictionary(int capacity) _inner = new Dictionary(capacity); } + /// + /// Initializes a new instance of the class using an IDictionary. + /// + public AvaloniaDictionary(IDictionary dictionary, IEqualityComparer? comparer = null) + { + if (dictionary != null) + { + _inner = new Dictionary(dictionary, comparer ?? EqualityComparer.Default); + } + else + { + throw new ArgumentNullException(nameof(dictionary)); + } + } + /// /// Occurs when the collection changes. /// diff --git a/tests/Avalonia.Base.UnitTests/Collections/AvaloniaDictionaryTests.cs b/tests/Avalonia.Base.UnitTests/Collections/AvaloniaDictionaryTests.cs index 739c3fed798..35fd9466705 100644 --- a/tests/Avalonia.Base.UnitTests/Collections/AvaloniaDictionaryTests.cs +++ b/tests/Avalonia.Base.UnitTests/Collections/AvaloniaDictionaryTests.cs @@ -1,8 +1,10 @@ using System; using System.Collections.Generic; using System.Collections.Specialized; + using Avalonia.Collections; using Avalonia.Data.Core; + using Xunit; namespace Avalonia.Base.UnitTests.Collections @@ -156,5 +158,34 @@ public void Clearing_Collection_Should_Raise_PropertyChanged() Assert.Equal(new[] { "Count", CommonPropertyNames.IndexerName }, tracker.Names); } + + [Fact] + public void Constructor_Should_Throw_ArgumentNullException_When_Collection_Is_Null() + { + Assert.Throws(() => + { + var target = new AvaloniaDictionary(null, null); + }); + } + + + [Fact] + public void Constructor_Should_Initialize_With_Provided_Collection() + { + var initialCollection = new Dictionary + { + { "key1", "value1" }, + { "key2", "value2" } + }; + + var target = new AvaloniaDictionary(initialCollection, null); + + Assert.Equal(2, target.Count); + Assert.Equal("value1", target["key1"]); + Assert.Equal("value2", target["key2"]); + } + + + } } From 2d8376c3bdbe8d611c918fb633d53120f48579a8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E3=81=A1=E3=83=BC=E3=81=9A=28=EF=BD=A58=EF=BD=A5=29?= =?UTF-8?q?=E3=81=91=E3=83=BC=E3=81=8D?= <31585494+MineCake147E@users.noreply.github.com> Date: Fri, 24 Jan 2025 00:10:12 +0900 Subject: [PATCH 10/16] `Imm32InputMethod.HandleComposition` no longer ignores IME cancel(`lParam` = 0) (#18034) --- src/Windows/Avalonia.Win32/Input/Imm32InputMethod.cs | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/src/Windows/Avalonia.Win32/Input/Imm32InputMethod.cs b/src/Windows/Avalonia.Win32/Input/Imm32InputMethod.cs index b1f2a77ca01..8a21d15120a 100644 --- a/src/Windows/Avalonia.Win32/Input/Imm32InputMethod.cs +++ b/src/Windows/Avalonia.Win32/Input/Imm32InputMethod.cs @@ -372,6 +372,11 @@ public void HandleComposition(IntPtr wParam, IntPtr lParam, uint timestamp) } var flags = (GCS)ToInt32(lParam); + + if (flags == 0) + { + CompositionChanged(""); + } if ((flags & GCS.GCS_RESULTSTR) != 0) { From 7638a61518cbc9eda472f15e3c38f384d23b461d Mon Sep 17 00:00:00 2001 From: Compunet <117437050+dme-compunet@users.noreply.github.com> Date: Fri, 24 Jan 2025 00:04:51 +0200 Subject: [PATCH 11/16] Get trailing whitespace length from glyph run metrics (#17960) Co-authored-by: Julien Lebosquain Co-authored-by: Benedikt Stebner --- .../Media/TextFormatting/TextFormatterImpl.cs | 27 +++---------------- 1 file changed, 3 insertions(+), 24 deletions(-) diff --git a/src/Avalonia.Base/Media/TextFormatting/TextFormatterImpl.cs b/src/Avalonia.Base/Media/TextFormatting/TextFormatterImpl.cs index 784ee835b40..71209286966 100644 --- a/src/Avalonia.Base/Media/TextFormatting/TextFormatterImpl.cs +++ b/src/Avalonia.Base/Media/TextFormatting/TextFormatterImpl.cs @@ -951,35 +951,14 @@ private static void ResetTrailingWhitespaceBidiLevels(RentedList lineTe return; } - var textSpan = shapedText.Text.Span; + var trailingWhitespaceLength = shapedText.GlyphRun.Metrics.TrailingWhitespaceLength; - if (textSpan.IsEmpty) + if (trailingWhitespaceLength == 0) { return; } - var whitespaceCharactersCount = 0; - - for (var i = textSpan.Length - 1; i >= 0; i--) - { - var isWhitespace = Codepoint.ReadAt(textSpan, i, out _).IsWhiteSpace; - - if (isWhitespace) - { - whitespaceCharactersCount++; - } - else - { - break; - } - } - - if (whitespaceCharactersCount == 0) - { - return; - } - - var splitIndex = shapedText.Length - whitespaceCharactersCount; + var splitIndex = shapedText.Length - trailingWhitespaceLength; var (textRuns, trailingWhitespaceRuns) = SplitTextRuns([shapedText], splitIndex, objectPool); From 01a7c859d4c4d46a23eb4c152217c106cd9318bb Mon Sep 17 00:00:00 2001 From: Julien Lebosquain Date: Fri, 24 Jan 2025 10:07:04 +0100 Subject: [PATCH 12/16] Fix style without selector not finding target type (#18026) * Add failing style test without selector * Fix XAML target type of style without selector * Address review * Throw for style without selector in ControlTheme --- .../AvaloniaXamlIlSelectorTransformer.cs | 55 +++++++++++++++++++ .../AvaloniaXamlIlWellKnownTypes.cs | 2 + .../Xaml/StyleTests.cs | 52 +++++++++++++++++- 3 files changed, 106 insertions(+), 3 deletions(-) diff --git a/src/Markup/Avalonia.Markup.Xaml.Loader/CompilerExtensions/Transformers/AvaloniaXamlIlSelectorTransformer.cs b/src/Markup/Avalonia.Markup.Xaml.Loader/CompilerExtensions/Transformers/AvaloniaXamlIlSelectorTransformer.cs index cfec681558b..e0d234e6df3 100644 --- a/src/Markup/Avalonia.Markup.Xaml.Loader/CompilerExtensions/Transformers/AvaloniaXamlIlSelectorTransformer.cs +++ b/src/Markup/Avalonia.Markup.Xaml.Loader/CompilerExtensions/Transformers/AvaloniaXamlIlSelectorTransformer.cs @@ -32,8 +32,26 @@ public IXamlAstNode Transform(AstTransformationContext context, IXamlAstNode nod var pn = on.Children.OfType() .FirstOrDefault(p => p.Property.GetClrProperty().Name == "Selector"); + // Missing selector, use the object's target type if available if (pn == null) + { + // We already went through this node + if (context.ParentNodes().FirstOrDefault() is AvaloniaXamlIlTargetTypeMetadataNode metadataNode + && metadataNode.Value == on) + { + return node; + } + + if (FindStyleParentObject(on, context) is { } parentObjectNode) + { + return new AvaloniaXamlIlTargetTypeMetadataNode( + on, + new XamlAstClrTypeReference(node, parentObjectNode.Type.GetClrType(), false), + AvaloniaXamlIlTargetTypeMetadataNode.ScopeTypes.Style); + } + return node; + } if (pn.Values.Count != 1) throw new XamlSelectorsTransformException("Selector property should have exactly one value", @@ -195,6 +213,20 @@ XamlIlSelectorNode Create(IEnumerable syntax, pn.Values[0] = selector; var templateType = GetLastTemplateTypeFromSelector(selector); + + // Empty selector, use the object's target type if available + if (selector == initialNode) + { + if (FindStyleParentObject(on, context) is { } parentObjectNode) + { + return new AvaloniaXamlIlTargetTypeMetadataNode( + on, + new XamlAstClrTypeReference(node, parentObjectNode.Type.GetClrType(), false), + AvaloniaXamlIlTargetTypeMetadataNode.ScopeTypes.Style); + } + + return node; + } var styleNode = new AvaloniaXamlIlTargetTypeMetadataNode(on, new XamlAstClrTypeReference(selector, selector.TargetType!, false), @@ -209,6 +241,29 @@ XamlIlSelectorNode Create(IEnumerable syntax, }; } + private static XamlAstObjectNode? FindStyleParentObject(XamlAstNode styleNode, AstTransformationContext context) + { + var avaloniaTypes = context.GetAvaloniaTypes(); + + var parentNode = context + .ParentNodes() + .OfType() + .FirstOrDefault(n => !avaloniaTypes.Styles.IsAssignableFrom(n.Type.GetClrType())); + + if (parentNode is not null) + { + var parentType = parentNode.Type.GetClrType(); + + if (avaloniaTypes.StyledElement.IsAssignableFrom(parentType)) + return parentNode; + + if (avaloniaTypes.ControlTheme.IsAssignableFrom(parentType)) + throw new XamlTransformException("Cannot add a Style without selector to a ControlTheme.", styleNode); + } + + return null; + } + private static IXamlType? GetLastTemplateTypeFromSelector(XamlIlSelectorNode? node) { while (node is not null) diff --git a/src/Markup/Avalonia.Markup.Xaml.Loader/CompilerExtensions/Transformers/AvaloniaXamlIlWellKnownTypes.cs b/src/Markup/Avalonia.Markup.Xaml.Loader/CompilerExtensions/Transformers/AvaloniaXamlIlWellKnownTypes.cs index 25c01354921..ed57fd008b5 100644 --- a/src/Markup/Avalonia.Markup.Xaml.Loader/CompilerExtensions/Transformers/AvaloniaXamlIlWellKnownTypes.cs +++ b/src/Markup/Avalonia.Markup.Xaml.Loader/CompilerExtensions/Transformers/AvaloniaXamlIlWellKnownTypes.cs @@ -126,6 +126,7 @@ sealed class AvaloniaXamlIlWellKnownTypes public IXamlType UriKind { get; } public IXamlConstructor UriConstructor { get; } public IXamlType Style { get; } + public IXamlType Styles { get; } public IXamlType ControlTheme { get; } public IXamlType WindowTransparencyLevel { get; } public IXamlType IReadOnlyListOfT { get; } @@ -325,6 +326,7 @@ public AvaloniaXamlIlWellKnownTypes(TransformerConfiguration cfg) UriKind = cfg.TypeSystem.GetType("System.UriKind"); UriConstructor = Uri.GetConstructor(new List() { cfg.WellKnownTypes.String, UriKind }); Style = cfg.TypeSystem.GetType("Avalonia.Styling.Style"); + Styles = cfg.TypeSystem.GetType("Avalonia.Styling.Styles"); ControlTheme = cfg.TypeSystem.GetType("Avalonia.Styling.ControlTheme"); ControlTemplate = cfg.TypeSystem.GetType("Avalonia.Markup.Xaml.Templates.ControlTemplate"); IReadOnlyListOfT = cfg.TypeSystem.GetType("System.Collections.Generic.IReadOnlyList`1"); diff --git a/tests/Avalonia.Markup.Xaml.UnitTests/Xaml/StyleTests.cs b/tests/Avalonia.Markup.Xaml.UnitTests/Xaml/StyleTests.cs index af12c58f26c..23bd85be799 100644 --- a/tests/Avalonia.Markup.Xaml.UnitTests/Xaml/StyleTests.cs +++ b/tests/Avalonia.Markup.Xaml.UnitTests/Xaml/StyleTests.cs @@ -15,6 +15,7 @@ namespace Avalonia.Markup.Xaml.UnitTests.Xaml { + [InvariantCulture] public class StyleTests : XamlTestBase { [Fact] @@ -713,9 +714,6 @@ public void Can_Binding_Classes_In_Setter() [Fact] public void Fails_Use_Classes_In_Setter_When_Selector_Is_Complex() { - // XmlException contains culture specific position message - Thread.CurrentThread.CurrentUICulture = CultureInfo.InvariantCulture; - using (UnitTestApplication.Start(TestServices.StyledWindow)) { var xaml = $""" @@ -739,5 +737,53 @@ public void Fails_Use_Classes_In_Setter_When_Selector_Is_Complex() Assert.Equal ("Cannot set Classes Binding property '(Classes.Banned)' because the style has an activator. Line 6, position 14.", exception.Message); } } + + [Theory] + [InlineData("")] + [InlineData("")] + [InlineData("")] + [InlineData("")] + public void No_Selector_Should_Target_Parent_Type(string styleStart, string styleEnd) + { + using var app = UnitTestApplication.Start(TestServices.StyledWindow); + + var window = (Window)AvaloniaRuntimeXamlLoader.Load( + $""" + + + {styleStart} + + {styleEnd} + + + """); + + Assert.Equal("title set via style!", window.Title); + } + + + [Theory] + [InlineData("")] + [InlineData("")] + public void No_Selector_Should_Fail_In_Control_Theme(string styleStart, string styleEnd) + { + using var app = UnitTestApplication.Start(TestServices.StyledWindow); + + var exception = Assert.ThrowsAny(() => (Window)AvaloniaRuntimeXamlLoader.Load( + $$""" + + + + {{styleStart}} + + {{styleEnd}} + + + + """)); + + Assert.Equal("Cannot add a Style without selector to a ControlTheme. Line 5, position 14.", exception.Message); + } } } From e175ff21e8cedb659d150bb8c415c238587b67e0 Mon Sep 17 00:00:00 2001 From: Dan Walmsley Date: Mon, 27 Jan 2025 19:41:41 +0000 Subject: [PATCH 13/16] Make sure MacOS uses the cached value for render scaling. (#18062) --- src/Avalonia.Native/TopLevelImpl.cs | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/src/Avalonia.Native/TopLevelImpl.cs b/src/Avalonia.Native/TopLevelImpl.cs index efda4bfade0..812307d991e 100644 --- a/src/Avalonia.Native/TopLevelImpl.cs +++ b/src/Avalonia.Native/TopLevelImpl.cs @@ -95,7 +95,7 @@ internal virtual void Init(MacOSTopLevelHandle handle) { _handle = handle; _savedLogicalSize = ClientSize; - _savedScaling = RenderScaling; + _savedScaling = Native?.Scaling ?? 1;; _nativeControlHost = new NativeControlHostImpl(Native!.CreateNativeControlHost()); _platformBehaviorInhibition = new PlatformBehaviorInhibition(Factory.CreatePlatformBehaviorInhibition()); _surfaces = new object[] { new GlPlatformSurface(Native), new MetalPlatformSurface(Native), this }; @@ -121,7 +121,8 @@ public Size ClientSize } } - public double RenderScaling => Native?.Scaling ?? 1; + + public double RenderScaling => _savedScaling; public IEnumerable Surfaces => _surfaces ?? Array.Empty(); public Action? Input { get; set; } public Action? Paint { get; set; } From 2e6c571fc9a90a07d00131cce8b8e7f40a09a2f8 Mon Sep 17 00:00:00 2001 From: Tom Edwards <109803929+TomEdwardsEnscape@users.noreply.github.com> Date: Tue, 28 Jan 2025 16:57:58 +0100 Subject: [PATCH 14/16] Added WrapItemsAlignment (#17792) --- src/Avalonia.Controls/WrapPanel.cs | 74 +++++++++++++++---- .../WrapPanelTests.cs | 55 ++++++++++++++ 2 files changed, 116 insertions(+), 13 deletions(-) diff --git a/src/Avalonia.Controls/WrapPanel.cs b/src/Avalonia.Controls/WrapPanel.cs index 1d6c78260d0..fa7386780f7 100644 --- a/src/Avalonia.Controls/WrapPanel.cs +++ b/src/Avalonia.Controls/WrapPanel.cs @@ -3,6 +3,7 @@ // // Licensed to The Avalonia Project under MIT License, courtesy of The .NET Foundation. +using System; using Avalonia.Input; using Avalonia.Layout; using Avalonia.Utilities; @@ -11,6 +12,24 @@ namespace Avalonia.Controls { + public enum WrapPanelItemsAlignment + { + /// + /// Items are laid out so the first one in each column/row touches the top/left of the panel. + /// + Start, + + /// + /// Items are laid out so that each column/row is centred vertically/horizontally within the panel. + /// + Center, + + /// + /// Items are laid out so the last one in each column/row touches the bottom/right of the panel. + /// + End, + } + /// /// Positions child elements in sequential position from left to right, /// breaking content to the next line at the edge of the containing box. @@ -25,6 +44,12 @@ public class WrapPanel : Panel, INavigableContainer public static readonly StyledProperty OrientationProperty = AvaloniaProperty.Register(nameof(Orientation), defaultValue: Orientation.Horizontal); + /// + /// Defines the property. + /// + public static readonly StyledProperty ItemsAlignmentProperty = + AvaloniaProperty.Register(nameof(ItemsAlignment), defaultValue: WrapPanelItemsAlignment.Start); + /// /// Defines the property. /// @@ -43,6 +68,7 @@ public class WrapPanel : Panel, INavigableContainer static WrapPanel() { AffectsMeasure(OrientationProperty, ItemWidthProperty, ItemHeightProperty); + AffectsArrange(ItemsAlignmentProperty); } /// @@ -54,6 +80,15 @@ public Orientation Orientation set => SetValue(OrientationProperty, value); } + /// + /// Gets or sets the alignment of items in the WrapPanel. + /// + public WrapPanelItemsAlignment ItemsAlignment + { + get => GetValue(ItemsAlignmentProperty); + set => SetValue(ItemsAlignmentProperty, value); + } + /// /// Gets or sets the width of all items in the WrapPanel. /// @@ -140,7 +175,7 @@ protected override Size MeasureOverride(Size constraint) var childConstraint = new Size( itemWidthSet ? itemWidth : constraint.Width, itemHeightSet ? itemHeight : constraint.Height); - + for (int i = 0, count = children.Count; i < count; i++) { var child = children[i]; @@ -205,7 +240,7 @@ protected override Size ArrangeOverride(Size finalSize) if (MathUtilities.GreaterThan(curLineSize.U + sz.U, uvFinalSize.U)) // Need to switch to another line { - ArrangeLine(accumulatedV, curLineSize.V, firstInLine, i, useItemU, itemU); + ArrangeLine(accumulatedV, curLineSize.V, firstInLine, i, useItemU, itemU, uvFinalSize.U); accumulatedV += curLineSize.V; curLineSize = sz; @@ -213,7 +248,7 @@ protected override Size ArrangeOverride(Size finalSize) if (MathUtilities.GreaterThan(sz.U, uvFinalSize.U)) // The element is wider then the constraint - give it a separate line { // Switch to next line which only contain one element - ArrangeLine(accumulatedV, sz.V, i, ++i, useItemU, itemU); + ArrangeLine(accumulatedV, sz.V, i, ++i, useItemU, itemU, uvFinalSize.U); accumulatedV += sz.V; curLineSize = new UVSize(orientation); @@ -230,31 +265,44 @@ protected override Size ArrangeOverride(Size finalSize) // Arrange the last line, if any if (firstInLine < children.Count) { - ArrangeLine(accumulatedV, curLineSize.V, firstInLine, children.Count, useItemU, itemU); + ArrangeLine(accumulatedV, curLineSize.V, firstInLine, children.Count, useItemU, itemU, uvFinalSize.U); } return finalSize; } - private void ArrangeLine(double v, double lineV, int start, int end, bool useItemU, double itemU) + private void ArrangeLine(double v, double lineV, int start, int end, bool useItemU, double itemU, double panelU) { var orientation = Orientation; var children = Children; double u = 0; bool isHorizontal = orientation == Orientation.Horizontal; + if (ItemsAlignment != WrapPanelItemsAlignment.Start) + { + double totalU = 0; + for (int i = start; i < end; i++) + { + totalU += GetChildU(i); + } + + u = ItemsAlignment switch + { + WrapPanelItemsAlignment.Center => (panelU - totalU) / 2, + WrapPanelItemsAlignment.End => panelU - totalU, + WrapPanelItemsAlignment.Start => 0, + _ => throw new NotImplementedException(), + }; + } + for (int i = start; i < end; i++) { - var child = children[i]; - var childSize = new UVSize(orientation, child.DesiredSize.Width, child.DesiredSize.Height); - double layoutSlotU = useItemU ? itemU : childSize.U; - child.Arrange(new Rect( - isHorizontal ? u : v, - isHorizontal ? v : u, - isHorizontal ? layoutSlotU : lineV, - isHorizontal ? lineV : layoutSlotU)); + double layoutSlotU = GetChildU(i); + children[i].Arrange(isHorizontal ? new(u, v, layoutSlotU, lineV) : new(v, u, lineV, layoutSlotU)); u += layoutSlotU; } + + double GetChildU(int i) => useItemU ? itemU : isHorizontal ? children[i].DesiredSize.Width : children[i].DesiredSize.Height; } private struct UVSize diff --git a/tests/Avalonia.Controls.UnitTests/WrapPanelTests.cs b/tests/Avalonia.Controls.UnitTests/WrapPanelTests.cs index 94f1b30fc90..c1066962a66 100644 --- a/tests/Avalonia.Controls.UnitTests/WrapPanelTests.cs +++ b/tests/Avalonia.Controls.UnitTests/WrapPanelTests.cs @@ -1,3 +1,4 @@ +using System; using Avalonia.Layout; using Xunit; @@ -47,6 +48,60 @@ public void Lays_Out_Horizontally_On_A_Single_Line() Assert.Equal(new Rect(100, 0, 100, 50), target.Children[1].Bounds); } + public static TheoryData GetItemsAlignmentValues() + { + var data = new TheoryData(); + foreach (var orientation in Enum.GetValues()) + { + foreach (var alignment in Enum.GetValues()) + { + data.Add(orientation, alignment); + } + } + return data; + } + + [Theory, MemberData(nameof(GetItemsAlignmentValues))] + public void Lays_Out_With_Items_Alignment(Orientation orientation, WrapPanelItemsAlignment itemsAlignment) + { + var target = new WrapPanel() + { + Width = 200, + Height = 200, + Orientation = orientation, + ItemsAlignment = itemsAlignment, + Children = + { + new Border { Height = 50, Width = 50 }, + new Border { Height = 50, Width = 50 }, + } + }; + + target.Measure(Size.Infinity); + target.Arrange(new Rect(target.DesiredSize)); + + Assert.Equal(new Size(200, 200), target.Bounds.Size); + + var rowBounds = target.Children[0].Bounds.Union(target.Children[1].Bounds); + + Assert.Equal(orientation switch + { + Orientation.Horizontal => new(100, 50), + Orientation.Vertical => new(50, 100), + _ => throw new NotImplementedException() + }, rowBounds.Size); + + Assert.Equal((orientation, itemsAlignment) switch + { + (_, WrapPanelItemsAlignment.Start) => new(0, 0), + (Orientation.Horizontal, WrapPanelItemsAlignment.Center) => new(50, 0), + (Orientation.Vertical, WrapPanelItemsAlignment.Center) => new(0, 50), + (Orientation.Horizontal, WrapPanelItemsAlignment.End) => new(100, 0), + (Orientation.Vertical, WrapPanelItemsAlignment.End) => new(0, 100), + _ => throw new NotImplementedException(), + }, rowBounds.Position); + } + [Fact] public void Lays_Out_Vertically_Children_On_A_Single_Line() { From 98a388d3a3249790a5865b79c8165e7745b2b93f Mon Sep 17 00:00:00 2001 From: Maxwell Katz Date: Tue, 28 Jan 2025 08:05:55 -0800 Subject: [PATCH 15/16] Add Popup.ShouldUseOverlayLayer property (#5629) * Implement Popup.ShouldUseOverlayLayer property * Add ShouldUseOverlayLayer tests --- .../Primitives/OverlayPopupHost.cs | 10 ++++- src/Avalonia.Controls/Primitives/Popup.cs | 39 ++++++++++++++++++- .../Primitives/PopupTests.cs | 32 +++++++++++++++ 3 files changed, 78 insertions(+), 3 deletions(-) diff --git a/src/Avalonia.Controls/Primitives/OverlayPopupHost.cs b/src/Avalonia.Controls/Primitives/OverlayPopupHost.cs index a7c7a17e6a3..3e4fee20b72 100644 --- a/src/Avalonia.Controls/Primitives/OverlayPopupHost.cs +++ b/src/Avalonia.Controls/Primitives/OverlayPopupHost.cs @@ -194,10 +194,16 @@ void IManagedPopupPositionerPopup.MoveAndResize(Point devicePoint, Size virtualS // TODO12: mark PrivateAPI or internal. [Unstable("PopupHost is considered an internal API. Use Popup or any Popup-based controls (Flyout, Tooltip) instead.")] public static IPopupHost CreatePopupHost(Visual target, IAvaloniaDependencyResolver? dependencyResolver) + => CreatePopupHost(target, dependencyResolver, false); + + internal static IPopupHost CreatePopupHost(Visual target, IAvaloniaDependencyResolver? dependencyResolver, bool shouldUseOverlayLayer) { - if (TopLevel.GetTopLevel(target) is { } topLevel && topLevel.PlatformImpl?.CreatePopup() is { } popupImpl) + if (!shouldUseOverlayLayer) { - return new PopupRoot(topLevel, popupImpl, dependencyResolver); + if (TopLevel.GetTopLevel(target) is { } topLevel && topLevel.PlatformImpl?.CreatePopup() is { } popupImpl) + { + return new PopupRoot(topLevel, popupImpl, dependencyResolver); + } } if (OverlayLayer.GetOverlayLayer(target) is { } overlayLayer) diff --git a/src/Avalonia.Controls/Primitives/Popup.cs b/src/Avalonia.Controls/Primitives/Popup.cs index 8f377f44c50..a90b1fb3b62 100644 --- a/src/Avalonia.Controls/Primitives/Popup.cs +++ b/src/Avalonia.Controls/Primitives/Popup.cs @@ -141,8 +141,21 @@ public class Popup : Control, IPopupHostProvider public static readonly AttachedProperty TakesFocusFromNativeControlProperty = AvaloniaProperty.RegisterAttached(nameof(TakesFocusFromNativeControl), true); + /// + /// Defines the property. + /// + public static readonly StyledProperty ShouldUseOverlayLayerProperty = + AvaloniaProperty.Register(nameof(ShouldUseOverlayLayer)); + + /// + /// Defines the property. + /// + public static readonly DirectProperty IsUsingOverlayLayerProperty = AvaloniaProperty.RegisterDirect( + nameof(IsUsingOverlayLayer), o => o.IsUsingOverlayLayer); + private bool _isOpenRequested; private bool _ignoreIsOpenChanged; + private bool _isUsingOverlayLayer; private PopupOpenState? _openState; private Action? _popupHostChangedHandler; @@ -386,6 +399,29 @@ public bool TakesFocusFromNativeControl set => SetValue(TakesFocusFromNativeControlProperty, value); } + /// + /// Gets or sets a value that indicates whether the popup should be shown in the overlay layer of the parent window. + /// + /// + /// When is "false" implementation depends on the platform. + /// Use to get actual popup behavior. + /// This is an equvalent of `OverlayPopups` property of the platform options, but settable independently per each popup. + /// + public bool ShouldUseOverlayLayer + { + get => GetValue(ShouldUseOverlayLayerProperty); + set => SetValue(ShouldUseOverlayLayerProperty, value); + } + + /// + /// Gets a value that indicates whether the popup is shown in the overlay layer of the parent window. + /// + public bool IsUsingOverlayLayer + { + get => _isUsingOverlayLayer; + private set => SetAndRaise(IsUsingOverlayLayerProperty, ref _isUsingOverlayLayer, value); + } + IPopupHost? IPopupHostProvider.PopupHost => Host; event Action? IPopupHostProvider.PopupHostChanged @@ -423,7 +459,7 @@ public void Open() _isOpenRequested = false; - var popupHost = OverlayPopupHost.CreatePopupHost(placementTarget, DependencyResolver); + var popupHost = OverlayPopupHost.CreatePopupHost(placementTarget, DependencyResolver, ShouldUseOverlayLayer); var handlerCleanup = new CompositeDisposable(7); UpdateHostSizing(popupHost, topLevel, placementTarget); @@ -541,6 +577,7 @@ public void Open() WindowManagerAddShadowHintChanged(popupHost, WindowManagerAddShadowHint); popupHost.Show(); + IsUsingOverlayLayer = popupHost is OverlayPopupHost; if (TakesFocusFromNativeControl) popupHost.TakeFocus(); diff --git a/tests/Avalonia.Controls.UnitTests/Primitives/PopupTests.cs b/tests/Avalonia.Controls.UnitTests/Primitives/PopupTests.cs index 29671f07665..86eb5f83a43 100644 --- a/tests/Avalonia.Controls.UnitTests/Primitives/PopupTests.cs +++ b/tests/Avalonia.Controls.UnitTests/Primitives/PopupTests.cs @@ -1260,6 +1260,38 @@ private static PopupRoot CreateRoot(TopLevel popupParent, IPopupImpl impl = null return result; } + [Fact] + public void Popup_Open_With_Correct_IsUsingOverlayLayer_And_Disabled_OverlayLayer() + { + using (CreateServices()) + { + var target = new Popup(); + target.IsOpen = true; + target.ShouldUseOverlayLayer = false; + + var window = PreparedWindow(target); + window.Show(); + + Assert.Equal(UsePopupHost, target.IsUsingOverlayLayer); + } + } + + [Fact] + public void Popup_Open_With_Correct_IsUsingOverlayLayer_And_Enabled_OverlayLayer() + { + using (CreateServices()) + { + var target = new Popup(); + target.IsOpen = true; + target.ShouldUseOverlayLayer = true; + + var window = PreparedWindow(target); + window.Show(); + + Assert.Equal(true, target.IsUsingOverlayLayer); + } + } + private IDisposable CreateServices() { return UnitTestApplication.Start(TestServices.StyledWindow.With( From 3e3f11d84f1d05fdf4ac4e9595648517b99438c5 Mon Sep 17 00:00:00 2001 From: Evan Ruiz Date: Wed, 29 Jan 2025 17:50:28 -0500 Subject: [PATCH 16/16] Path Geometry update fix for 4748 bug rerender on change for paths segments (#18025) * Add failing Path Geometry update test #4748 [BUG] Rerender on change for Paths, Segments and e.g. Failing Test * Fixes failing Path Geometry update test Fixes #4748 [BUG] Rerender on change for Paths, Segments and e.g. --- src/Avalonia.Base/Media/PathGeometry.cs | 2 +- .../Media/PathGeometryTests.cs | 32 +++++++++++++++++++ 2 files changed, 33 insertions(+), 1 deletion(-) create mode 100644 tests/Avalonia.Base.UnitTests/Media/PathGeometryTests.cs diff --git a/src/Avalonia.Base/Media/PathGeometry.cs b/src/Avalonia.Base/Media/PathGeometry.cs index bdfbfadce4d..c07535c00b3 100644 --- a/src/Avalonia.Base/Media/PathGeometry.cs +++ b/src/Avalonia.Base/Media/PathGeometry.cs @@ -35,7 +35,7 @@ static PathGeometry() /// public PathGeometry() { - _figures = new PathFigures(); + Figures = new PathFigures(); } /// diff --git a/tests/Avalonia.Base.UnitTests/Media/PathGeometryTests.cs b/tests/Avalonia.Base.UnitTests/Media/PathGeometryTests.cs new file mode 100644 index 00000000000..9648187e61e --- /dev/null +++ b/tests/Avalonia.Base.UnitTests/Media/PathGeometryTests.cs @@ -0,0 +1,32 @@ +using Avalonia.Media; +using Xunit; + +namespace Avalonia.Base.UnitTests.Media; + +public class PathGeometryTests +{ + [Fact] + public void PathGeometry_Triggers_Invalidation_On_Figures_Add() + { + var segment = new PolyLineSegment() + { + Points = [new Point(1, 1), new Point(2, 2)] + }; + + var figure = new PathFigure() + { + Segments = [segment], + IsClosed = false, + IsFilled = false, + }; + + var target = new PathGeometry(); + + var changed = false; + + target.Changed += (_, _) => { changed = true; }; + + target.Figures?.Add(figure); + Assert.True(changed); + } +}