Skip to content
Merged
Show file tree
Hide file tree
Changes from 5 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
20 changes: 20 additions & 0 deletions .github/copilot-instructions.md
Original file line number Diff line number Diff line change
Expand Up @@ -138,6 +138,26 @@ When working with public API changes:
- **Use `dotnet format analyzers`** if having trouble
- **If files are incorrect**: Revert all changes, then add only the necessary new API entries

**🚨 CRITICAL: `#nullable enable` must be line 1**

Every `PublicAPI.Unshipped.txt` file starts with `#nullable enable` (often BOM-prefixed: `#nullable enable`) on the **first line**. If this line is moved or removed, the analyzer treats it as a declared API symbol and emits **RS0017** errors.

**Never sort these files with plain `sort`** — the BOM bytes (`0xEF 0xBB 0xBF`) sort after ASCII characters under `LC_ALL=C`, pushing `#nullable enable` to the bottom of the file.

When resolving merge conflicts or adding entries, use this safe pattern that preserves line 1:
```bash
for f in $(git diff --name-only --diff-filter=U | grep "PublicAPI.Unshipped.txt"); do
# Extract and preserve the #nullable enable line (with or without BOM)
HEADER=$(head -1 "$f" | grep -o '.*#nullable enable' || echo '#nullable enable')
# Strip conflict markers, remove all #nullable lines, sort+dedup the API entries
grep -v '^<<<<<<\|^======\|^>>>>>>\|#nullable enable' "$f" | LC_ALL=C sort -u | sed '/^$/d' > /tmp/api_fix.txt
# Reassemble: header first, then sorted entries
printf '%s\n' "$HEADER" > "$f"
cat /tmp/api_fix.txt >> "$f"
git add "$f"
done
```

### Branching
- `main` - For bug fixes without API changes
- `net10.0` - For new features and API changes
Expand Down
1 change: 1 addition & 0 deletions eng/pipelines/ci-device-tests.yml
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ trigger:
- release/*
- net*.0
- inflight/*
- darc-*
tags:
include:
- '*'
Expand Down
1 change: 1 addition & 0 deletions eng/pipelines/ci-uitests.yml
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ trigger:
- release/*
- net*.0
- inflight/*
- darc-*
tags:
include:
- '*'
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,14 @@ public override void OnBindViewHolder(RecyclerView.ViewHolder holder, int positi
return;
}

// Header and footer view holders should not participate in selection tracking.
// They are not data items and calling GetItem() on their positions would cause
// an ArgumentOutOfRangeException due to the header index adjustment.
if (ItemsSource.IsHeader(position) || ItemsSource.IsFooter(position))
{
return;
}

// Watch for clicks so the user can select the item held by this ViewHolder
selectable.Clicked += SelectableClicked;

Expand Down
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
81 changes: 81 additions & 0 deletions src/Controls/tests/TestCases.HostApp/Issues/Issue34120.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,81 @@
namespace Maui.Controls.Sample.Issues;

[Issue(IssueTracker.Github, 34120, "Label text truncated in ScrollView when MaxLines is set", PlatformAffected.Android)]
public class Issue34120 : ContentPage
{
// Reproduces the N3_Navigation layout: horizontal ScrollView with BindableLayout,
// 200×200 Border cards, Image (HeightRequest=120), and a Label with MaxLines=2.
record Issue34120MonkeyItem(string Name, string ImageUrl);

public Issue34120()
{
// Long names ("Golden Snub-nosed Monkey", "Tonkin Snub-nosed Monkey") are the ones
// that triggered truncation; "Baboon" is a short-name reference card.
var monkeys = new List<Issue34120MonkeyItem>
{
new("Golden Snub-nosed Monkey", "golden.jpg"),
new("Baboon", "papio.jpg"),
new("Tonkin Snub-nosed Monkey", "bluemonkey.jpg"),
new("Howler Monkey", "alouatta.jpg"),
new("Squirrel Monkey", "saimiri.jpg"),
};

var itemTemplate = new DataTemplate(() =>
{
var image = new Image
{
Aspect = Aspect.AspectFit,
HeightRequest = 120,
HorizontalOptions = LayoutOptions.Center,
VerticalOptions = LayoutOptions.Center,
};
image.SetBinding(Image.SourceProperty, "ImageUrl");

var nameLabel = new Label
{
FontSize = 14,
FontAttributes = FontAttributes.Bold,
BackgroundColor = Color.FromArgb("#AAFFFFFF"),
TextColor = Colors.Black,
Padding = new Thickness(4, 2),
HorizontalOptions = LayoutOptions.Center,
VerticalOptions = LayoutOptions.Center,
HorizontalTextAlignment = TextAlignment.Center,
LineBreakMode = LineBreakMode.WordWrap,
MaxLines = 2,
};
nameLabel.SetBinding(Label.TextProperty, "Name");
nameLabel.SetBinding(Label.AutomationIdProperty, "Name");

var card = new Border
{
Padding = new Thickness(10),
Stroke = Colors.LightGray,
StrokeThickness = 1,
WidthRequest = 200,
HeightRequest = 200,
BackgroundColor = Colors.White,
Content = new VerticalStackLayout
{
Spacing = 10,
Children = { image, nameLabel }
}
};
return card;
});

var horizontalStack = new HorizontalStackLayout
{
Spacing = 15,
Padding = new Thickness(5),
};
BindableLayout.SetItemTemplate(horizontalStack, itemTemplate);
BindableLayout.SetItemsSource(horizontalStack, monkeys);

Content = new ScrollView
{
Orientation = ScrollOrientation.Horizontal,
Content = horizontalStack,
};
}
}
58 changes: 58 additions & 0 deletions src/Controls/tests/TestCases.HostApp/Issues/Issue34247.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
using System.Collections.ObjectModel;

namespace Maui.Controls.Sample.Issues;

[Issue(IssueTracker.Github, 34247, "CollectionView with HeaderTemplate and SelectionMode.Single crashes on selection", PlatformAffected.All)]
Comment thread
NirmalKumarYuvaraj marked this conversation as resolved.
Outdated
public class Issue34247 : ContentPage
{
public Issue34247()
{
var layout = new StackLayout();

var resultLabel = new Label()
{
Text = "Select an item to test",
AutomationId = "ResultLabel"
};

var items = new ObservableCollection<string> { "Item 1", "Item 2", "Item 3" };

var collectionView = new CollectionView()
{
SelectionMode = SelectionMode.Single,
AutomationId = "TestCollectionView",
HeightRequest = 300
};

collectionView.HeaderTemplate = new DataTemplate(() =>
{
return new Label
{
Text = "Header",
FontSize = 18,
FontAttributes = FontAttributes.Bold,
Margin = new Thickness(10)
};
});

collectionView.ItemTemplate = new DataTemplate(() =>
{
var label = new Label();
label.SetBinding(Label.TextProperty, ".");
label.Margin = new Thickness(10);
label.FontSize = 16;
return label;
});

collectionView.ItemsSource = items;
collectionView.SelectionChanged += (s, e) =>
{
resultLabel.Text = "Success";
};

layout.Children.Add(resultLabel);
layout.Children.Add(collectionView);

Content = layout;
}
}
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
using NUnit.Framework;
using UITest.Appium;
using UITest.Core;

namespace Microsoft.Maui.TestCases.Tests.Issues;

public class Issue34120 : _IssuesUITest
{
public override string Issue => "Label text truncated in ScrollView when MaxLines is set";

public Issue34120(TestDevice device) : base(device) { }

[Test]
[Category(UITestCategories.Label)]
public void LabelNotTruncatedWithMaxLines()
{
// Wait for the page to load, then verify labels are not truncated.
App.WaitForElement("Golden Snub-nosed Monkey");
VerifyScreenshot();
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
using NUnit.Framework;
using UITest.Appium;
using UITest.Core;

namespace Microsoft.Maui.TestCases.Tests.Issues
{
public class Issue34247 : _IssuesUITest
{
public Issue34247(TestDevice testDevice) : base(testDevice)
{
}

public override string Issue => "CollectionView with HeaderTemplate and SelectionMode.Single crashes on selection";

[Test]
[Category(UITestCategories.CollectionView)]
public void SelectingItemInCollectionViewWithHeaderTemplateDoesNotCrash()
{
App.WaitForElement("TestCollectionView");
App.WaitForElement("Item 1");
App.Tap("Item 1");
var result = App.WaitForElement("ResultLabel").GetText();
Assert.That(result, Is.EqualTo("Success"));
Comment thread
NirmalKumarYuvaraj marked this conversation as resolved.
}
}
}
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
31 changes: 30 additions & 1 deletion src/Core/src/Handlers/Label/LabelHandler.Android.cs
Original file line number Diff line number Diff line change
Expand Up @@ -22,9 +22,12 @@ public override Size GetDesiredSize(double widthConstraint, double heightConstra

// Android TextView reports full available width instead of actual text width when
// text wraps to multiple lines, causing incorrect positioning for non-Fill alignments.
// We narrow the desired width to the widest rendered line, but only when that narrowing
// won't cause re-wrapping that exceeds MaxLines and truncates visible text.
if (VirtualView.HorizontalLayoutAlignment != Primitives.LayoutAlignment.Fill &&
PlatformView?.Layout is Layout layout &&
layout.LineCount > 1)
layout.LineCount > 1 &&
PlatformView.Ellipsize == null)
{
float maxLineWidth = 0;
for (int i = 0; i < layout.LineCount; i++)
Expand All @@ -38,7 +41,33 @@ public override Size GetDesiredSize(double widthConstraint, double heightConstra
{
var actualWidth = Context.FromPixels(maxLineWidth + PlatformView.PaddingLeft + PlatformView.PaddingRight);
if (actualWidth < size.Width)
{
// When MaxLines is constrained, verify that narrowing doesn't cause the text
// to re-wrap into more lines than MaxLines allows (which would truncate text).
// Re-measure at exactly the pixel width the view will be arranged at.
if (PlatformView.MaxLines != int.MaxValue)
{
var narrowedPx = (int)Context.ToPixels(actualWidth);

// AtMost mirrors how the layout pass constrains width, ensuring the
// re-measurement reflects the same wrapping behaviour the view will
// experience when arranged at actualWidth.
PlatformView.Measure(
MeasureSpecMode.AtMost.MakeMeasureSpec(narrowedPx),
MeasureSpecMode.Unspecified.MakeMeasureSpec(0));

// Fail-safe: if Layout is null after re-measurement we cannot verify
// that truncation won't occur, so return the original size.
var measuredLayout = PlatformView.Layout;

if (measuredLayout is null || measuredLayout.LineCount > PlatformView.MaxLines)
{
return size; // Narrowing causes truncation (or unverifiable); return original size
}
}

return new Size(actualWidth, size.Height);
}
}
}

Expand Down
2 changes: 1 addition & 1 deletion src/Core/src/Platform/Android/PlatformTouchGraphicsView.cs
Original file line number Diff line number Diff line change
Expand Up @@ -89,7 +89,7 @@ public void TouchesMoved(PointF[] points)
{
if (!_dragStarted)
{
if (points.Length == 1)
if (points.Length == 1 && _lastMovedViewPoints.Length > 0)
{
float deltaX = _lastMovedViewPoints[0].X - points[0].X;
float deltaY = _lastMovedViewPoints[0].Y - points[0].Y;
Expand Down