Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[Proposal] Windows support for IconColorTintBehavior #1182

Closed
3 tasks done
kperdlich opened this issue May 11, 2023 · 7 comments · Fixed by #1179
Closed
3 tasks done

[Proposal] Windows support for IconColorTintBehavior #1182

kperdlich opened this issue May 11, 2023 · 7 comments · Fixed by #1179
Assignees
Labels
approved This Proposal has been approved and is ready to be added to the Toolkit champion A member of the .NET MAUI Toolkit core team has chosen to champion this feature documentation approved proposal A fully fleshed out proposal describing a new feature in syntactic and semantic detail

Comments

@kperdlich
Copy link
Contributor

kperdlich commented May 11, 2023

Feature name

Windows support for IconColorTintBehavior

Link to discussion

#1180

Progress tracker

Summary

Windows support for IconColorTintBehvior.

Motivation

  • Better support for windows
  • Easier to port an iOS/Android app to windows

Detailed Design

Adds windows support for the IconTintColorBehavior e.g. using the Windows.UI.Composition API.

IconTintColorBehavior.windows.cs
using System.ComponentModel;
using System.Numerics;
using Microsoft.Maui.Platform;
using Microsoft.UI.Composition;
using Microsoft.UI.Xaml;
using Microsoft.UI.Xaml.Hosting;
using Microsoft.UI.Xaml.Media;
using Microsoft.UI.Xaml.Media.Imaging;
using WImage = Microsoft.UI.Xaml.Controls.Image;
using WButton = Microsoft.UI.Xaml.Controls.Button;

namespace CommunityToolkit.Maui.Behaviors;

public partial class IconTintColorBehavior
{
    SpriteVisual? spriteVisual;
    Vector2? originalImageSize;
    bool IsUpdate => originalImageSize is not null;

    /// <inheritdoc/>
    protected override void OnAttachedTo(View bindable, FrameworkElement platformView)
    {
  	  ApplyTintColor(platformView, bindable, TintColor);

  	  bindable.PropertyChanged += OnElementPropertyChanged;
  	  this.PropertyChanged += (s, e) =>
  	  {
  		  if (e.PropertyName == TintColorProperty.PropertyName)
  		  {
  			  ApplyTintColor(platformView, bindable, TintColor);
  		  }
  	  };
    }

    /// <inheritdoc/>
    protected override void OnDetachedFrom(View bindable, FrameworkElement platformView)
    {
  	  bindable.PropertyChanged -= OnElementPropertyChanged;
  	  RemoveTintColor(platformView);
  	  originalImageSize = null;
    }

    void OnElementPropertyChanged(object? sender, PropertyChangedEventArgs e)
    {
  	  if (e.PropertyName is not string propertyName
  	      || sender is not View bindable
  	      || bindable.Handler?.PlatformView is not FrameworkElement platformView)
  	  {
  		  return;
  	  }

  	  if (!propertyName.Equals(Image.SourceProperty.PropertyName, StringComparison.Ordinal)
  	      && !propertyName.Equals(ImageButton.SourceProperty.PropertyName, StringComparison.Ordinal))
  	  {
  		  return;
  	  }

  	  ApplyTintColor(platformView, bindable, TintColor);
    }

    void ApplyTintColor(FrameworkElement platformView, View element, Color? color)
    {
  	  if (color is null)
  	  {
  		  RemoveTintColor(platformView);
  		  return;
  	  }

  	  switch (platformView)
  	  {
  		  case WImage image:
  		  {
  			  LoadAndApplyImageTintColor(element, image, color);
  			  break;
  		  }
  		  case WButton button:
  		  {
  			  var image = TryGetButtonImage(button);
  			  if (image is null)
  			  {
  				  return;
  			  }

  			  LoadAndApplyImageTintColor(element, image, color);
  			  break;
  		  }
  		  default:
  			  throw new NotSupportedException(
  				  $"{nameof(IconTintColorBehavior)} only currently supports {nameof(WImage)} and {nameof(WButton)}.");
  	  }
    }

    void LoadAndApplyImageTintColor(View element, WImage image, Color color)
    {
  	  // There seems to be no other indicator if the image is loaded and the ActualSize is available.
  	  var isLoaded = image.ActualSize != Vector2.Zero;

  	  if (isLoaded || IsUpdate)
  	  {
  		  ApplyImageTintColor(element, image, color);
  	  }
  	  else
  	  {
  		  void OnButtonImageSizeInitialized(object sender, SizeChangedEventArgs e)
  		  {
  			  image.SizeChanged -= OnButtonImageSizeInitialized;
  			  ApplyImageTintColor(element, image, color);
  		  }

  		  image.SizeChanged += OnButtonImageSizeInitialized;
  	  }
    }


    void ApplyImageTintColor(View element, WImage image, Color color)
    {
  	  var uri = TryGetSourceImageUri(image, element as IImageElement);
  	  if (uri is null)
  	  {
  		  return;
  	  }

  	  originalImageSize = GetTintImageSize(image);
  	  var width = originalImageSize.Value.X;
  	  var height = originalImageSize.Value.Y;

  	  // Hide possible visible pixels from original image.
  	  // Workaround since the tinted image is added as a child to the current image and it's not possible to hide a parent without hiding its children using Visibility.Collapsed.
  	  image.Width = image.Height = 0;

  	  // Workaround requires offset to re-center tinted image.
  	  var offset = new Vector3(-width * .5f, -height * .5f, 0f);

  	  ApplyTintCompositionEffect(image, color, width, height, offset, uri);
    }

    Vector2 GetTintImageSize(WImage image)
    {
  	  // ActualSize is set by the layout process when loaded. Without the zero size workaround, it's always what we want (default). 
  	  if (image.ActualSize != Vector2.Zero)
  	  {
  		  return image.ActualSize;
  	  }

  	  if (originalImageSize.HasValue)
  	  {
  		  return originalImageSize.Value;
  	  }

  	  return new Vector2((float)image.Width, (float)image.Height);
    }

    void ApplyTintCompositionEffect(FrameworkElement platformView, Color color, float width, float height,
  	  Vector3 offset, Uri surfaceMaskUri)
    {
  	  var compositor = ElementCompositionPreview.GetElementVisual(platformView).Compositor;

  	  var sourceColorBrush = compositor.CreateColorBrush();
  	  sourceColorBrush.Color = color.ToWindowsColor();

  	  var loadedSurfaceMask = LoadedImageSurface.StartLoadFromUri(surfaceMaskUri);

  	  var maskBrush = compositor.CreateMaskBrush();
  	  maskBrush.Source = sourceColorBrush;
  	  maskBrush.Mask = compositor.CreateSurfaceBrush(loadedSurfaceMask);

  	  spriteVisual = compositor.CreateSpriteVisual();
  	  spriteVisual.Brush = maskBrush;
  	  spriteVisual.Size = new Vector2(width, height);
  	  spriteVisual.Offset = offset;

  	  ElementCompositionPreview.SetElementChildVisual(platformView, spriteVisual);
    }

    void RemoveTintColor(FrameworkElement platformView)
    {
  	  if (spriteVisual is null)
  	  {
  		  return;
  	  }

  	  switch (platformView)
  	  {
  		  case WImage image:
  		  {
  			  RestoreOriginalImageSize(image);
  			  break;
  		  }
  		  case WButton button:
  		  {
  			  var image = TryGetButtonImage(button);
  			  if (image is not null)
  			  {
  				  RestoreOriginalImageSize(image);
  			  }
  			  break;
  		  }
  	  }

  	  spriteVisual.Brush = null;
  	  spriteVisual = null;
  	  ElementCompositionPreview.SetElementChildVisual(platformView, null);
    }

    void RestoreOriginalImageSize(WImage image)
    {
  	  if (originalImageSize is null)
  	  {
  		  return;
  	  }

  	  // Restore in Width/Height since ActualSize is readonly
  	  image.Width = originalImageSize.Value.X;
  	  image.Height = originalImageSize.Value.Y;
    }

    static Uri? TryGetSourceImageUri(WImage? imageControl, IImageElement? imageElement)
    {
  	  if (imageElement?.Source is UriImageSource uriImageSource)
  	  {
  		  return uriImageSource.Uri;
  	  }

  	  if (imageControl?.Source is BitmapImage bitmapImage)
  	  {
  		  return bitmapImage.UriSource;
  	  }

  	  return null;
    }

    static WImage? TryGetButtonImage(WButton button)
    {
  	  return button.Content as WImage;
    }
}

Usage Syntax

Drawbacks

No response

Alternatives

No response

Unresolved Questions

No response

@kperdlich kperdlich added new proposal A fully fleshed out proposal describing a new feature in syntactic and semantic detail labels May 11, 2023
@brminnick
Copy link
Collaborator

Thanks @kperdlich!

Could you please provide a Detailed Design and Usage Syntax to allow us to review your Proposal?

You can reference other Proposals to better understand the detail we are seeking: #306

@kperdlich kperdlich changed the title [Proposal] Windows support for IconColorTintBehvior [Proposal] Windows support for IconColorTintBehavior May 11, 2023
@kperdlich
Copy link
Contributor Author

@brminnick The IconTintColorBehavior currently only supports Android, iOS and mac and this would add support for windows to increase platform support. It won't change anything on the API.

@brminnick
Copy link
Collaborator

Yes, I understand that. I've copy/pasted the implementation from the PR into the Detailed Design section for you.

@brminnick brminnick self-assigned this May 18, 2023
@ghost ghost added champion A member of the .NET MAUI Toolkit core team has chosen to champion this feature and removed new labels May 18, 2023
@VladislavAntonyuk
Copy link
Collaborator

approve

@brminnick
Copy link
Collaborator

I approve this feature ✅

@bijington
Copy link
Contributor

Approve

@ghost ghost added approved This Proposal has been approved and is ready to be added to the Toolkit help wanted This proposal has been approved and is ready to be implemented labels May 18, 2023
@brminnick brminnick removed the help wanted This proposal has been approved and is ready to be implemented label May 22, 2023
@ghost
Copy link

ghost commented May 22, 2023

Reopening Proposal.

Only Proposals moved to the Closed Project Column and Completed Project Column can be closed.

@ghost ghost reopened this May 22, 2023
@ghost ghost closed this as completed May 27, 2023
@github-actions github-actions bot locked and limited conversation to collaborators Nov 22, 2024
This issue was closed.
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.
Labels
approved This Proposal has been approved and is ready to be added to the Toolkit champion A member of the .NET MAUI Toolkit core team has chosen to champion this feature documentation approved proposal A fully fleshed out proposal describing a new feature in syntactic and semantic detail
Projects
None yet
Development

Successfully merging a pull request may close this issue.

4 participants