diff --git a/README.md b/README.md index afb92e3..10634d9 100644 --- a/README.md +++ b/README.md @@ -108,7 +108,11 @@ public static MauiApp CreateMauiApp() ## Platform-specific Implementations -`SelectableLabel` uses platform-specific handlers to achieve cross-platform compatibility. On Android, it leverages `TextView` with selectable properties, while on iOS and MacCatalyst, `UITextView` provides similar functionality. +`SelectableLabel` uses platform-specific handlers to achieve cross-platform compatibility: + +- **Android**: Uses `TextView` with `SetTextIsSelectable(true)` for text selection functionality +- **iOS & MacCatalyst**: Uses `UITextView` with `Selectable = true` and `Editable = false` +- **Windows**: Uses `RichTextBlock` with `IsTextSelectionEnabled = true` for WinUI 3 compatibility ## Contributing diff --git a/samples/Indiko.Maui.Controls.SelectableLabel.Sample/Indiko.Maui.Controls.SelectableLabel.Sample.csproj b/samples/Indiko.Maui.Controls.SelectableLabel.Sample/Indiko.Maui.Controls.SelectableLabel.Sample.csproj index a9bac84..f012d6c 100644 --- a/samples/Indiko.Maui.Controls.SelectableLabel.Sample/Indiko.Maui.Controls.SelectableLabel.Sample.csproj +++ b/samples/Indiko.Maui.Controls.SelectableLabel.Sample/Indiko.Maui.Controls.SelectableLabel.Sample.csproj @@ -1,7 +1,7 @@  - net9.0-android;net9.0-ios + net8.0-android;net8.0-ios;net8.0-windows10.0.19041.0 Exe Indiko.Maui.Controls.SelectableLabel.Sample true @@ -21,6 +21,7 @@ 14.2 30.0 + 10.0.17763.0 @@ -44,8 +45,8 @@ - - + + diff --git a/samples/Indiko.Maui.Controls.SelectableLabel.Sample/Platforms/Windows/App.xaml b/samples/Indiko.Maui.Controls.SelectableLabel.Sample/Platforms/Windows/App.xaml new file mode 100644 index 0000000..349e60b --- /dev/null +++ b/samples/Indiko.Maui.Controls.SelectableLabel.Sample/Platforms/Windows/App.xaml @@ -0,0 +1,12 @@ + + + + + + + + + \ No newline at end of file diff --git a/samples/Indiko.Maui.Controls.SelectableLabel.Sample/Platforms/Windows/App.xaml.cs b/samples/Indiko.Maui.Controls.SelectableLabel.Sample/Platforms/Windows/App.xaml.cs new file mode 100644 index 0000000..dcc4e10 --- /dev/null +++ b/samples/Indiko.Maui.Controls.SelectableLabel.Sample/Platforms/Windows/App.xaml.cs @@ -0,0 +1,23 @@ +using Microsoft.UI.Xaml; + +// To learn more about WinUI, the WinUI project structure, +// and more about our project templates, see: http://aka.ms/winui-project-info. + +namespace Indiko.Maui.Controls.SelectableLabel.Sample.WinUI; + +/// +/// Provides application-specific behavior to supplement the default Application class. +/// +public partial class App : MauiWinUIApplication +{ + /// + /// Initializes the singleton application object. This is the first line of authored code + /// executed, and as such is the logical equivalent of main() or WinMain(). + /// + public App() + { + this.InitializeComponent(); + } + + protected override MauiApp CreateMauiApp() => MauiProgram.CreateMauiApp(); +} \ No newline at end of file diff --git a/samples/Indiko.Maui.Controls.SelectableLabel.Sample/Platforms/Windows/app.manifest b/samples/Indiko.Maui.Controls.SelectableLabel.Sample/Platforms/Windows/app.manifest new file mode 100644 index 0000000..9b0bed3 --- /dev/null +++ b/samples/Indiko.Maui.Controls.SelectableLabel.Sample/Platforms/Windows/app.manifest @@ -0,0 +1,5 @@ + + + false + + \ No newline at end of file diff --git a/src/Indiko.Maui.Controls.SelectableLabel/BuilderExtension.cs b/src/Indiko.Maui.Controls.SelectableLabel/BuilderExtension.cs index 81fe7e6..5bb22e9 100644 --- a/src/Indiko.Maui.Controls.SelectableLabel/BuilderExtension.cs +++ b/src/Indiko.Maui.Controls.SelectableLabel/BuilderExtension.cs @@ -11,6 +11,10 @@ using Indiko.Maui.Controls.SelectableLabel.Platforms.MacCatalyst; #endif +#if WINDOWS +using Indiko.Maui.Controls.SelectableLabel.Platforms.Windows; +#endif + namespace Indiko.Maui.Controls.SelectableLabel; public static class BuilderExtension diff --git a/src/Indiko.Maui.Controls.SelectableLabel/Indiko.Maui.Controls.SelectableLabel.csproj b/src/Indiko.Maui.Controls.SelectableLabel/Indiko.Maui.Controls.SelectableLabel.csproj index ada0314..e2cda19 100644 --- a/src/Indiko.Maui.Controls.SelectableLabel/Indiko.Maui.Controls.SelectableLabel.csproj +++ b/src/Indiko.Maui.Controls.SelectableLabel/Indiko.Maui.Controls.SelectableLabel.csproj @@ -1,7 +1,7 @@  - net9.0-android;net9.0-ios;net9.0-maccatalyst + net8.0-android;net8.0-ios;net8.0-maccatalyst;net8.0-windows10.0.19041.0 true true enable @@ -9,6 +9,7 @@ 14.2 14.0 21.0 + 10.0.17763.0 INDIKO Copyright © INDIKO and contributors @@ -30,7 +31,7 @@ - + @@ -38,7 +39,7 @@ - + diff --git a/src/Indiko.Maui.Controls.SelectableLabel/Platforms/Windows/SelectableLabelHandler.cs b/src/Indiko.Maui.Controls.SelectableLabel/Platforms/Windows/SelectableLabelHandler.cs new file mode 100644 index 0000000..44666db --- /dev/null +++ b/src/Indiko.Maui.Controls.SelectableLabel/Platforms/Windows/SelectableLabelHandler.cs @@ -0,0 +1,270 @@ +using Microsoft.Maui.Handlers; +using Microsoft.Maui.Platform; +using Microsoft.UI; +using Microsoft.UI.Text; +using Microsoft.UI.Xaml.Controls; +using Microsoft.UI.Xaml.Documents; +using Microsoft.UI.Xaml.Media; +using System.Text; + +namespace Indiko.Maui.Controls.SelectableLabel.Platforms.Windows; + +public class SelectableLabelHandler : ViewHandler +{ + public static readonly IPropertyMapper Mapper = + new PropertyMapper(ViewHandler.ViewMapper) + { + [nameof(SelectableLabel.Text)] = MapText, + [nameof(SelectableLabel.TextColor)] = MapTextColor, + [nameof(SelectableLabel.FontAttributes)] = MapFontAttributes, + [nameof(SelectableLabel.FontSize)] = MapFontSize, + [nameof(SelectableLabel.FontFamily)] = MapFontFamily, + [nameof(SelectableLabel.BackgroundColor)] = MapBackgroundColor, + [nameof(SelectableLabel.LineBreakMode)] = MapLineBreakMode, + [nameof(SelectableLabel.TextDecorations)] = MapTextDecorations, + [nameof(SelectableLabel.TextTransform)] = MapTextTransform, + [nameof(SelectableLabel.LineHeight)] = MapLineHeight, + [nameof(SelectableLabel.MaxLines)] = MapMaxLines, + [nameof(SelectableLabel.HorizontalTextAlignment)] = MapHorizontalTextAlignment, + [nameof(SelectableLabel.VerticalTextAlignment)] = MapVerticalTextAlignment, + [nameof(SelectableLabel.CharacterSpacing)] = MapCharacterSpacing, + [nameof(SelectableLabel.FormattedText)] = MapFormattedText + }; + + public SelectableLabelHandler() : base(Mapper) + { + + } + + protected override RichTextBlock CreatePlatformView() + { + var richTextBlock = new RichTextBlock + { + IsTextSelectionEnabled = true, + TextWrapping = Microsoft.UI.Xaml.TextWrapping.Wrap + }; + return richTextBlock; + } + + public static void MapText(SelectableLabelHandler handler, SelectableLabel label) + { + handler.PlatformView.Blocks.Clear(); + + if (!string.IsNullOrEmpty(label.Text)) + { + var paragraph = new Paragraph(); + var run = new Run { Text = label.Text }; + paragraph.Inlines.Add(run); + handler.PlatformView.Blocks.Add(paragraph); + } + } + + public static void MapTextColor(SelectableLabelHandler handler, SelectableLabel label) + { + handler.PlatformView.Foreground = new SolidColorBrush(label.TextColor.ToWindowsColor()); + } + + public static void MapFontAttributes(SelectableLabelHandler handler, SelectableLabel label) + { + var fontWeight = label.FontAttributes.HasFlag(FontAttributes.Bold) ? FontWeights.Bold : FontWeights.Normal; + var fontStyle = label.FontAttributes.HasFlag(FontAttributes.Italic) ? Microsoft.UI.Text.FontStyle.Italic : Microsoft.UI.Text.FontStyle.Normal; + + handler.PlatformView.FontWeight = fontWeight; + handler.PlatformView.FontStyle = fontStyle; + } + + public static void MapFontSize(SelectableLabelHandler handler, SelectableLabel label) + { + if (label.FontSize > 0) + { + handler.PlatformView.FontSize = label.FontSize; + } + } + + public static void MapFontFamily(SelectableLabelHandler handler, SelectableLabel label) + { + if (!string.IsNullOrEmpty(label.FontFamily)) + { + handler.PlatformView.FontFamily = new Microsoft.UI.Xaml.Media.FontFamily(label.FontFamily); + } + } + + public static void MapBackgroundColor(SelectableLabelHandler handler, SelectableLabel label) + { + handler.PlatformView.Background = new SolidColorBrush(label.BackgroundColor.ToWindowsColor()); + } + + public static void MapLineBreakMode(SelectableLabelHandler handler, SelectableLabel label) + { + handler.PlatformView.TextWrapping = label.LineBreakMode switch + { + LineBreakMode.NoWrap => Microsoft.UI.Xaml.TextWrapping.NoWrap, + LineBreakMode.WordWrap => Microsoft.UI.Xaml.TextWrapping.Wrap, + LineBreakMode.CharacterWrap => Microsoft.UI.Xaml.TextWrapping.WrapWholeWords, + LineBreakMode.HeadTruncation or LineBreakMode.TailTruncation or LineBreakMode.MiddleTruncation => Microsoft.UI.Xaml.TextWrapping.NoWrap, + _ => Microsoft.UI.Xaml.TextWrapping.Wrap + }; + + handler.PlatformView.TextTrimming = label.LineBreakMode switch + { + LineBreakMode.HeadTruncation => Microsoft.UI.Xaml.TextTrimming.WordEllipsis, + LineBreakMode.TailTruncation => Microsoft.UI.Xaml.TextTrimming.CharacterEllipsis, + LineBreakMode.MiddleTruncation => Microsoft.UI.Xaml.TextTrimming.WordEllipsis, + _ => Microsoft.UI.Xaml.TextTrimming.None + }; + } + + public static void MapTextDecorations(SelectableLabelHandler handler, SelectableLabel label) + { + var textDecorations = Microsoft.UI.Text.TextDecorations.None; + + if (label.TextDecorations.HasFlag(TextDecorations.Underline)) + { + textDecorations |= Microsoft.UI.Text.TextDecorations.Underline; + } + + if (label.TextDecorations.HasFlag(TextDecorations.Strikethrough)) + { + textDecorations |= Microsoft.UI.Text.TextDecorations.Strikethrough; + } + + handler.PlatformView.TextDecorations = textDecorations; + } + + public static void MapTextTransform(SelectableLabelHandler handler, SelectableLabel label) + { + if (!string.IsNullOrEmpty(label.Text)) + { + var transformedText = ApplyTextTransform(label.Text, label.TextTransform); + + handler.PlatformView.Blocks.Clear(); + var paragraph = new Paragraph(); + var run = new Run { Text = transformedText }; + paragraph.Inlines.Add(run); + handler.PlatformView.Blocks.Add(paragraph); + } + } + + public static void MapLineHeight(SelectableLabelHandler handler, SelectableLabel label) + { + if (label.LineHeight > 0) + { + handler.PlatformView.LineHeight = label.LineHeight * handler.PlatformView.FontSize; + } + } + + public static void MapMaxLines(SelectableLabelHandler handler, SelectableLabel label) + { + if (label.MaxLines > 0) + { + handler.PlatformView.MaxLines = label.MaxLines; + } + else + { + handler.PlatformView.MaxLines = 0; // No limit + } + } + + public static void MapHorizontalTextAlignment(SelectableLabelHandler handler, SelectableLabel label) + { + handler.PlatformView.TextAlignment = label.HorizontalTextAlignment switch + { + TextAlignment.Start => Microsoft.UI.Xaml.TextAlignment.Left, + TextAlignment.Center => Microsoft.UI.Xaml.TextAlignment.Center, + TextAlignment.End => Microsoft.UI.Xaml.TextAlignment.Right, + _ => Microsoft.UI.Xaml.TextAlignment.Left + }; + } + + public static void MapVerticalTextAlignment(SelectableLabelHandler handler, SelectableLabel label) + { + handler.PlatformView.VerticalAlignment = label.VerticalTextAlignment switch + { + TextAlignment.Start => Microsoft.UI.Xaml.VerticalAlignment.Top, + TextAlignment.Center => Microsoft.UI.Xaml.VerticalAlignment.Center, + TextAlignment.End => Microsoft.UI.Xaml.VerticalAlignment.Bottom, + _ => Microsoft.UI.Xaml.VerticalAlignment.Top + }; + } + + public static void MapCharacterSpacing(SelectableLabelHandler handler, SelectableLabel label) + { + if (label.CharacterSpacing >= 0) + { + handler.PlatformView.CharacterSpacing = (int)(label.CharacterSpacing * 1000); // Convert em to 1000th of an em + } + } + + public static void MapFormattedText(SelectableLabelHandler handler, SelectableLabel label) + { + if (label.FormattedText != null) + { + handler.PlatformView.Blocks.Clear(); + var paragraph = new Paragraph(); + + foreach (var span in label.FormattedText.Spans) + { + var transformedText = ApplyTextTransform(span.Text, span.TextTransform); + var run = new Run { Text = transformedText }; + + // Apply font attributes + if (span.FontAttributes.HasFlag(FontAttributes.Bold)) + { + run.FontWeight = FontWeights.Bold; + } + if (span.FontAttributes.HasFlag(FontAttributes.Italic)) + { + run.FontStyle = Microsoft.UI.Text.FontStyle.Italic; + } + + // Apply font size + if (span.FontSize > 0) + { + run.FontSize = span.FontSize; + } + + // Apply text color + if (span.TextColor != null) + { + run.Foreground = new SolidColorBrush(span.TextColor.ToWindowsColor()); + } + + // Apply text decorations + var textDecorations = Microsoft.UI.Text.TextDecorations.None; + if (span.TextDecorations.HasFlag(TextDecorations.Underline)) + { + textDecorations |= Microsoft.UI.Text.TextDecorations.Underline; + } + if (span.TextDecorations.HasFlag(TextDecorations.Strikethrough)) + { + textDecorations |= Microsoft.UI.Text.TextDecorations.Strikethrough; + } + run.TextDecorations = textDecorations; + + // Apply character spacing + if (span.CharacterSpacing >= 0) + { + run.CharacterSpacing = (int)(span.CharacterSpacing * 1000); + } + + // Note: GestureRecognizers are not supported in the same way on Windows RichTextBlock + // This would require additional implementation using Hyperlink elements + + paragraph.Inlines.Add(run); + } + + handler.PlatformView.Blocks.Add(paragraph); + } + } + + private static string ApplyTextTransform(string text, TextTransform transform) + { + return transform switch + { + TextTransform.Uppercase => text?.ToUpper(), + TextTransform.Lowercase => text?.ToLower(), + //TextTransform.Capitalize => CultureInfo.CurrentCulture.TextInfo.ToTitleCase(text ?? string.Empty), + _ => text + }; + } +} \ No newline at end of file diff --git a/src/Indiko.Maui.Controls.SelectableLabel/SelectableLabel.cs b/src/Indiko.Maui.Controls.SelectableLabel/SelectableLabel.cs index 8ab9e1c..e74328c 100644 --- a/src/Indiko.Maui.Controls.SelectableLabel/SelectableLabel.cs +++ b/src/Indiko.Maui.Controls.SelectableLabel/SelectableLabel.cs @@ -64,7 +64,7 @@ public TextDecorations TextDecorations set => SetValue(TextDecorationsProperty, value); } - public static readonly BindableProperty TextTransformProperty = BindableProperty.Create(nameof(TextDecorations), typeof(TextTransform), typeof(SelectableLabel), TextTransform.None); + public static readonly BindableProperty TextTransformProperty = BindableProperty.Create(nameof(TextTransform), typeof(TextTransform), typeof(SelectableLabel), TextTransform.None); public TextTransform TextTransform { @@ -80,7 +80,7 @@ public double LineHeight set => SetValue(LineHeightProperty, value); } - public static readonly BindableProperty MaxLinesProperty = BindableProperty.Create(nameof(LineHeight), typeof(int), typeof(SelectableLabel), -1); + public static readonly BindableProperty MaxLinesProperty = BindableProperty.Create(nameof(MaxLines), typeof(int), typeof(SelectableLabel), -1); public int MaxLines { get => (int)GetValue(MaxLinesProperty);