Skip to content
Merged
Show file tree
Hide file tree
Changes from all 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
56 changes: 43 additions & 13 deletions src/Core/src/Platform/iOS/TextFieldExtensions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -229,31 +229,61 @@ internal static void UpdateClearButtonColor(this UITextField textField, IEntry e
{
if (entry.TextColor is null)
{
// Release any custom image so UIKit restores its own system clear button appearance.
// Setting TintColor to null alone is not enough — the template image persists.
// Setting TintColor to null allows the system to automatically apply the appropriate color based on the current theme (light or dark mode)
clearButton.TintColor = null;
// SetImage(null) releases the custom tinted bitmap so UIKit restores its system default.
// The color path (else branch) reads ImageForState(.Highlighted) to get that original
// image as the source for tinting. Without these calls, TintColor=null has no visual effect.
clearButton.SetImage(null, UIControlState.Normal);
clearButton.SetImage(null, UIControlState.Highlighted);
clearButton.TintColor = null;
}
else
{
// Use AlwaysTemplate rendering so UIKit fills the icon at full opacity with TintColor,
// bypassing the semi-transparent pixels baked into the system clear button icon.
UIImage? sourceImage = clearButton.ImageForState(UIControlState.Normal)
?? clearButton.ImageForState(UIControlState.Highlighted);
// On a null→color transition, UIKit restores the system image after SetImage(null),
// so ImageForState(Highlighted) returns the system clear button image as the tinting source.
UIImage? defaultClearImage = clearButton.ImageForState(UIControlState.Highlighted);
clearButton.TintColor = entry.TextColor.ToPlatform();

if (sourceImage is not null)
var tintedClearImage = GetClearButtonTintImage(defaultClearImage, entry.TextColor.ToPlatform());
if (tintedClearImage is not null)
{
UIImage templateImage = sourceImage.ImageWithRenderingMode(UIImageRenderingMode.AlwaysTemplate);
clearButton.SetImage(templateImage, UIControlState.Normal);
clearButton.SetImage(templateImage, UIControlState.Highlighted);
clearButton.SetImage(tintedClearImage, UIControlState.Normal);
clearButton.SetImage(tintedClearImage, UIControlState.Highlighted);
}

clearButton.TintColor = entry.TextColor.ToPlatform();
}
}
}

internal static UIImage? GetClearButtonTintImage(UIImage? image, UIColor color)
{
if (image is null)
{
return null;
}

var size = image.Size;

var renderer = new UIGraphicsImageRenderer(size, new UIGraphicsImageRendererFormat()
{
Opaque = false,
Scale = UIScreen.MainScreen.Scale,
});

if (renderer is null)
{
return null;
}

return renderer.CreateImage((context) =>
{
image.Draw(CGPoint.Empty, CGBlendMode.Normal, 1.0f);
color.ColorWithAlpha(1.0f).SetFill();

var rect = new CGRect(CGPoint.Empty.X, CGPoint.Empty.Y, image.Size.Width, image.Size.Height);
context?.FillRect(rect, CGBlendMode.SourceIn);
});
}

internal static void AddMauiDoneAccessoryView(this UITextField textField, IViewHandler handler)
{
#if !MACCATALYST
Expand Down
40 changes: 2 additions & 38 deletions src/Core/tests/DeviceTests/Handlers/Entry/EntryHandlerTests.iOS.cs
Original file line number Diff line number Diff line change
Expand Up @@ -121,7 +121,7 @@ await AttachAndRun(entry, async (handler) =>

var tintedImage = clearButton.ImageForState(UIControlState.Normal);
Assert.NotNull(tintedImage);
Assert.Equal(UIImageRenderingMode.AlwaysTemplate, tintedImage.RenderingMode);
Assert.Equal(UIImageRenderingMode.Automatic, tintedImage.RenderingMode);

entry.TextColor = null;
handler.UpdateValue(nameof(IEntry.TextColor));
Expand All @@ -139,7 +139,7 @@ await AttachAndRun(entry, async (handler) =>
// Confirms ImageForState(.Highlighted) returns the original after SetImage(null)
var retintedImage = clearButton.ImageForState(UIControlState.Normal);
Assert.NotNull(retintedImage);
Assert.Equal(UIImageRenderingMode.AlwaysTemplate, retintedImage.RenderingMode);
Assert.Equal(UIImageRenderingMode.Automatic, retintedImage.RenderingMode);
});
}

Expand Down Expand Up @@ -809,42 +809,6 @@ double GetNativeCharacterSpacing(EntryHandler entryHandler)
return entry.AttributedText.GetCharacterSpacing();
}

[Fact(DisplayName = "Entry ClearButton uses template rendering tinted to TextColor")]
public async Task EntryClearButtonUsesTemplateRenderingTintedToTextColor()
{
var entry = new EntryStub
{
Text = "hello",
TextColor = Colors.Blue,
ClearButtonVisibility = ClearButtonVisibility.WhileEditing,
};

await InvokeOnMainThreadAsync(async () =>
{
var handler = CreateHandler<EntryHandler>(entry);
var textField = GetNativeEntry(handler);

await textField.AttachAndRun(async () =>
{
textField.BecomeFirstResponder();

UIButton clearButton = null;
await AssertEventually(() =>
{
clearButton = textField.ValueForKey(new NSString("clearButton")) as UIButton;
return clearButton?.ImageForState(UIControlState.Normal) is not null;
}, timeout: 2000);

Assert.NotNull(clearButton);
Assert.Equal(Colors.Blue.ToPlatform(), clearButton.TintColor);

var image = clearButton.ImageForState(UIControlState.Normal);
Assert.NotNull(image);
Assert.Equal(UIImageRenderingMode.AlwaysTemplate, image.RenderingMode);
});
});
}

static UITextField GetNativeEntry(EntryHandler entryHandler) =>
(UITextField)entryHandler.PlatformView;

Expand Down
Loading