2025-12-19 09:30:16 +00:00
|
|
|
// Licensed to the .NET Foundation under one or more agreements.
|
|
|
|
|
// The .NET Foundation licenses this file to you under the MIT license.
|
|
|
|
|
|
2026-01-16 05:31:38 +00:00
|
|
|
using System.Windows.Input;
|
|
|
|
|
using Microsoft.Maui.Controls;
|
2026-01-16 04:39:50 +00:00
|
|
|
using Microsoft.Maui.Graphics;
|
2025-12-19 09:30:16 +00:00
|
|
|
using Microsoft.Maui.Platform.Linux.Rendering;
|
2026-01-16 04:39:50 +00:00
|
|
|
using SkiaSharp;
|
2025-12-19 09:30:16 +00:00
|
|
|
|
|
|
|
|
namespace Microsoft.Maui.Platform;
|
|
|
|
|
|
|
|
|
|
/// <summary>
|
|
|
|
|
/// Skia-rendered search bar control.
|
2026-01-16 05:31:38 +00:00
|
|
|
/// Implements MAUI ISearchBar interface patterns.
|
2025-12-19 09:30:16 +00:00
|
|
|
/// </summary>
|
|
|
|
|
public class SkiaSearchBar : SkiaView
|
|
|
|
|
{
|
2026-01-16 05:31:38 +00:00
|
|
|
#region Fields
|
|
|
|
|
|
2025-12-19 09:30:16 +00:00
|
|
|
private readonly SkiaEntry _entry;
|
|
|
|
|
private bool _showClearButton;
|
|
|
|
|
|
2026-01-16 05:31:38 +00:00
|
|
|
#endregion
|
|
|
|
|
|
|
|
|
|
#region Properties
|
|
|
|
|
|
2025-12-19 09:30:16 +00:00
|
|
|
public string Text
|
|
|
|
|
{
|
|
|
|
|
get => _entry.Text;
|
|
|
|
|
set => _entry.Text = value;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
public string Placeholder
|
|
|
|
|
{
|
|
|
|
|
get => _entry.Placeholder;
|
|
|
|
|
set => _entry.Placeholder = value;
|
|
|
|
|
}
|
|
|
|
|
|
2026-01-16 04:39:50 +00:00
|
|
|
public Color TextColor
|
2025-12-19 09:30:16 +00:00
|
|
|
{
|
|
|
|
|
get => _entry.TextColor;
|
|
|
|
|
set => _entry.TextColor = value;
|
|
|
|
|
}
|
|
|
|
|
|
2026-01-16 04:39:50 +00:00
|
|
|
public Color PlaceholderColor
|
2025-12-19 09:30:16 +00:00
|
|
|
{
|
|
|
|
|
get => _entry.PlaceholderColor;
|
|
|
|
|
set => _entry.PlaceholderColor = value;
|
|
|
|
|
}
|
|
|
|
|
|
2026-01-16 05:31:38 +00:00
|
|
|
public Color SearchBarBackgroundColor { get; set; } = Color.FromRgb(245, 245, 245);
|
|
|
|
|
public Color IconColor { get; set; } = Color.FromRgb(117, 117, 117);
|
|
|
|
|
public Color ClearButtonColor { get; set; } = Color.FromRgb(158, 158, 158);
|
|
|
|
|
public Color FocusedBorderColor { get; set; } = Color.FromRgb(33, 150, 243);
|
|
|
|
|
|
|
|
|
|
public string FontFamily
|
|
|
|
|
{
|
|
|
|
|
get => _entry.FontFamily;
|
|
|
|
|
set => _entry.FontFamily = value;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
public double FontSize
|
|
|
|
|
{
|
|
|
|
|
get => _entry.FontSize;
|
|
|
|
|
set => _entry.FontSize = value;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
public FontAttributes FontAttributes
|
|
|
|
|
{
|
|
|
|
|
get => _entry.FontAttributes;
|
|
|
|
|
set => _entry.FontAttributes = value;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
public double CharacterSpacing
|
|
|
|
|
{
|
|
|
|
|
get => _entry.CharacterSpacing;
|
|
|
|
|
set => _entry.CharacterSpacing = value;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
public TextAlignment HorizontalTextAlignment
|
|
|
|
|
{
|
|
|
|
|
get => _entry.HorizontalTextAlignment;
|
|
|
|
|
set => _entry.HorizontalTextAlignment = value;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
public double CornerRadius { get; set; } = 8.0;
|
|
|
|
|
public double IconSize { get; set; } = 20.0;
|
|
|
|
|
|
|
|
|
|
public ICommand? SearchCommand { get; set; }
|
|
|
|
|
public object? SearchCommandParameter { get; set; }
|
|
|
|
|
|
|
|
|
|
#endregion
|
|
|
|
|
|
|
|
|
|
#region Events
|
2025-12-19 09:30:16 +00:00
|
|
|
|
|
|
|
|
public event EventHandler<TextChangedEventArgs>? TextChanged;
|
|
|
|
|
public event EventHandler? SearchButtonPressed;
|
|
|
|
|
|
2026-01-16 05:31:38 +00:00
|
|
|
#endregion
|
|
|
|
|
|
|
|
|
|
#region Constructor
|
|
|
|
|
|
2025-12-19 09:30:16 +00:00
|
|
|
public SkiaSearchBar()
|
|
|
|
|
{
|
|
|
|
|
_entry = new SkiaEntry
|
|
|
|
|
{
|
|
|
|
|
Placeholder = "Search...",
|
2026-01-16 04:39:50 +00:00
|
|
|
EntryBackgroundColor = Colors.Transparent,
|
2026-01-17 03:10:29 +00:00
|
|
|
BackgroundColor = Colors.Transparent,
|
2026-01-16 04:39:50 +00:00
|
|
|
BorderColor = Colors.Transparent,
|
|
|
|
|
FocusedBorderColor = Colors.Transparent,
|
2026-01-24 01:53:26 +00:00
|
|
|
BorderWidth = 0,
|
|
|
|
|
VerticalTextAlignment = TextAlignment.Center
|
2025-12-19 09:30:16 +00:00
|
|
|
};
|
|
|
|
|
|
|
|
|
|
_entry.TextChanged += (s, e) =>
|
|
|
|
|
{
|
|
|
|
|
_showClearButton = !string.IsNullOrEmpty(e.NewTextValue);
|
|
|
|
|
TextChanged?.Invoke(this, e);
|
|
|
|
|
Invalidate();
|
|
|
|
|
};
|
|
|
|
|
|
2026-01-16 05:31:38 +00:00
|
|
|
_entry.Completed += (s, e) =>
|
|
|
|
|
{
|
|
|
|
|
SearchButtonPressed?.Invoke(this, EventArgs.Empty);
|
|
|
|
|
if (SearchCommand?.CanExecute(SearchCommandParameter) == true)
|
|
|
|
|
{
|
|
|
|
|
SearchCommand.Execute(SearchCommandParameter);
|
|
|
|
|
}
|
|
|
|
|
};
|
2025-12-19 09:30:16 +00:00
|
|
|
|
|
|
|
|
IsFocusable = true;
|
|
|
|
|
}
|
|
|
|
|
|
2026-01-16 05:31:38 +00:00
|
|
|
#endregion
|
|
|
|
|
|
|
|
|
|
#region Drawing
|
|
|
|
|
|
2025-12-19 09:30:16 +00:00
|
|
|
protected override void OnDraw(SKCanvas canvas, SKRect bounds)
|
|
|
|
|
{
|
2026-01-16 05:31:38 +00:00
|
|
|
float iconPadding = 12f;
|
|
|
|
|
float clearButtonSize = 20f;
|
|
|
|
|
float cornerRadius = (float)CornerRadius;
|
|
|
|
|
float iconSize = (float)IconSize;
|
2025-12-19 09:30:16 +00:00
|
|
|
|
2026-01-24 02:21:56 +00:00
|
|
|
// Draw background - use theme-aware color if not explicitly set
|
|
|
|
|
var bgColor = SearchBarBackgroundColor.ToSKColor();
|
|
|
|
|
// If using default light color, check for dark mode
|
|
|
|
|
if (SearchBarBackgroundColor.Red > 0.9f && SearchBarBackgroundColor.Green > 0.9f && SearchBarBackgroundColor.Blue > 0.9f)
|
|
|
|
|
{
|
|
|
|
|
bgColor = SkiaTheme.InputBackgroundSK;
|
|
|
|
|
}
|
2025-12-19 09:30:16 +00:00
|
|
|
using var bgPaint = new SKPaint
|
|
|
|
|
{
|
2026-01-24 02:21:56 +00:00
|
|
|
Color = bgColor,
|
2025-12-19 09:30:16 +00:00
|
|
|
IsAntialias = true,
|
|
|
|
|
Style = SKPaintStyle.Fill
|
|
|
|
|
};
|
|
|
|
|
|
2026-01-16 05:31:38 +00:00
|
|
|
var bgRect = new SKRoundRect(bounds, cornerRadius);
|
2025-12-19 09:30:16 +00:00
|
|
|
canvas.DrawRoundRect(bgRect, bgPaint);
|
|
|
|
|
|
|
|
|
|
// Draw focus border
|
|
|
|
|
if (IsFocused || _entry.IsFocused)
|
|
|
|
|
{
|
|
|
|
|
using var borderPaint = new SKPaint
|
|
|
|
|
{
|
2026-01-17 03:36:37 +00:00
|
|
|
Color = FocusedBorderColor.ToSKColor(),
|
2025-12-19 09:30:16 +00:00
|
|
|
IsAntialias = true,
|
|
|
|
|
Style = SKPaintStyle.Stroke,
|
|
|
|
|
StrokeWidth = 2
|
|
|
|
|
};
|
|
|
|
|
canvas.DrawRoundRect(bgRect, borderPaint);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Draw search icon
|
|
|
|
|
var iconX = bounds.Left + iconPadding;
|
|
|
|
|
var iconY = bounds.MidY;
|
2026-01-16 05:31:38 +00:00
|
|
|
DrawSearchIcon(canvas, iconX, iconY, iconSize);
|
2025-12-19 09:30:16 +00:00
|
|
|
|
|
|
|
|
// Calculate entry bounds - leave space for clear button
|
2026-01-16 05:31:38 +00:00
|
|
|
var entryLeft = iconX + iconSize + iconPadding;
|
2025-12-19 09:30:16 +00:00
|
|
|
var entryRight = _showClearButton
|
|
|
|
|
? bounds.Right - clearButtonSize - iconPadding * 2
|
|
|
|
|
: bounds.Right - iconPadding;
|
|
|
|
|
|
2026-01-17 05:22:37 +00:00
|
|
|
var entryBounds = new Rect(entryLeft, bounds.Top, entryRight - entryLeft, bounds.Height);
|
2025-12-19 09:30:16 +00:00
|
|
|
_entry.Arrange(entryBounds);
|
|
|
|
|
_entry.Draw(canvas);
|
|
|
|
|
|
|
|
|
|
// Draw clear button
|
|
|
|
|
if (_showClearButton)
|
|
|
|
|
{
|
|
|
|
|
var clearX = bounds.Right - iconPadding - clearButtonSize / 2;
|
|
|
|
|
var clearY = bounds.MidY;
|
|
|
|
|
DrawClearButton(canvas, clearX, clearY, clearButtonSize / 2);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private void DrawSearchIcon(SKCanvas canvas, float x, float y, float size)
|
|
|
|
|
{
|
|
|
|
|
using var paint = new SKPaint
|
|
|
|
|
{
|
2026-01-17 03:36:37 +00:00
|
|
|
Color = IconColor.ToSKColor(),
|
2025-12-19 09:30:16 +00:00
|
|
|
IsAntialias = true,
|
|
|
|
|
Style = SKPaintStyle.Stroke,
|
|
|
|
|
StrokeWidth = 2,
|
|
|
|
|
StrokeCap = SKStrokeCap.Round
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
var circleRadius = size * 0.35f;
|
|
|
|
|
var circleCenter = new SKPoint(x + circleRadius, y - circleRadius * 0.3f);
|
|
|
|
|
|
|
|
|
|
// Draw magnifying glass circle
|
|
|
|
|
canvas.DrawCircle(circleCenter, circleRadius, paint);
|
|
|
|
|
|
|
|
|
|
// Draw handle
|
|
|
|
|
var handleStart = new SKPoint(
|
|
|
|
|
circleCenter.X + circleRadius * 0.7f,
|
|
|
|
|
circleCenter.Y + circleRadius * 0.7f);
|
|
|
|
|
var handleEnd = new SKPoint(
|
|
|
|
|
x + size * 0.8f,
|
|
|
|
|
y + size * 0.3f);
|
|
|
|
|
canvas.DrawLine(handleStart, handleEnd, paint);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private void DrawClearButton(SKCanvas canvas, float x, float y, float radius)
|
|
|
|
|
{
|
|
|
|
|
// Draw circle background
|
|
|
|
|
using var bgPaint = new SKPaint
|
|
|
|
|
{
|
2026-01-17 03:36:37 +00:00
|
|
|
Color = ClearButtonColor.ToSKColor().WithAlpha(80),
|
2025-12-19 09:30:16 +00:00
|
|
|
IsAntialias = true,
|
|
|
|
|
Style = SKPaintStyle.Fill
|
|
|
|
|
};
|
|
|
|
|
canvas.DrawCircle(x, y, radius + 2, bgPaint);
|
|
|
|
|
|
|
|
|
|
// Draw X
|
|
|
|
|
using var paint = new SKPaint
|
|
|
|
|
{
|
2026-01-17 03:36:37 +00:00
|
|
|
Color = ClearButtonColor.ToSKColor(),
|
2025-12-19 09:30:16 +00:00
|
|
|
IsAntialias = true,
|
|
|
|
|
Style = SKPaintStyle.Stroke,
|
|
|
|
|
StrokeWidth = 2,
|
|
|
|
|
StrokeCap = SKStrokeCap.Round
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
var offset = radius * 0.5f;
|
|
|
|
|
canvas.DrawLine(x - offset, y - offset, x + offset, y + offset, paint);
|
|
|
|
|
canvas.DrawLine(x + offset, y - offset, x - offset, y + offset, paint);
|
|
|
|
|
}
|
|
|
|
|
|
2026-01-16 05:31:38 +00:00
|
|
|
#endregion
|
|
|
|
|
|
|
|
|
|
#region Input Handling
|
|
|
|
|
|
2025-12-19 09:30:16 +00:00
|
|
|
public override void OnPointerPressed(PointerEventArgs e)
|
|
|
|
|
{
|
|
|
|
|
if (!IsEnabled) return;
|
|
|
|
|
|
|
|
|
|
// Convert to local coordinates (relative to this view's bounds)
|
|
|
|
|
var localX = e.X - Bounds.Left;
|
|
|
|
|
|
|
|
|
|
// Check if clear button was clicked (in the rightmost 40 pixels)
|
|
|
|
|
if (_showClearButton && localX >= Bounds.Width - 40)
|
|
|
|
|
{
|
|
|
|
|
Text = "";
|
|
|
|
|
Invalidate();
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
2025-12-21 13:26:56 -05:00
|
|
|
// Forward to entry for text input focus and selection
|
2025-12-19 09:30:16 +00:00
|
|
|
_entry.IsFocused = true;
|
|
|
|
|
IsFocused = true;
|
2025-12-21 13:26:56 -05:00
|
|
|
_entry.OnPointerPressed(e);
|
2025-12-19 09:30:16 +00:00
|
|
|
Invalidate();
|
|
|
|
|
}
|
|
|
|
|
|
2025-12-21 13:26:56 -05:00
|
|
|
public override void OnPointerMoved(PointerEventArgs e)
|
|
|
|
|
{
|
|
|
|
|
if (!IsEnabled) return;
|
|
|
|
|
_entry.OnPointerMoved(e);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
public override void OnPointerReleased(PointerEventArgs e)
|
|
|
|
|
{
|
|
|
|
|
_entry.OnPointerReleased(e);
|
|
|
|
|
}
|
|
|
|
|
|
2025-12-19 09:30:16 +00:00
|
|
|
public override void OnTextInput(TextInputEventArgs e)
|
|
|
|
|
{
|
|
|
|
|
_entry.OnTextInput(e);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
public override void OnKeyDown(KeyEventArgs e)
|
|
|
|
|
{
|
|
|
|
|
if (e.Key == Key.Escape && _showClearButton)
|
|
|
|
|
{
|
|
|
|
|
Text = "";
|
|
|
|
|
e.Handled = true;
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
_entry.OnKeyDown(e);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
public override void OnKeyUp(KeyEventArgs e)
|
|
|
|
|
{
|
|
|
|
|
_entry.OnKeyUp(e);
|
|
|
|
|
}
|
|
|
|
|
|
2026-01-16 05:31:38 +00:00
|
|
|
#endregion
|
|
|
|
|
|
|
|
|
|
#region Measurement
|
|
|
|
|
|
2026-01-17 05:22:37 +00:00
|
|
|
protected override Size MeasureOverride(Size availableSize)
|
2025-12-19 09:30:16 +00:00
|
|
|
{
|
2026-01-17 05:22:37 +00:00
|
|
|
return new Size(250, 40);
|
2025-12-19 09:30:16 +00:00
|
|
|
}
|
2026-01-16 05:31:38 +00:00
|
|
|
|
|
|
|
|
#endregion
|
2025-12-19 09:30:16 +00:00
|
|
|
}
|