Skip to content

Commit

Permalink
Implement AppThemeColor, AppThemeObject, and AppThemeResource (#1264)
Browse files Browse the repository at this point in the history
* Implement AppThemeColor and AppThemeResource

* Undo AvatarView sample changes

* Add XML docs and use .shared. filename convention

* Add samples

* Split AppThemeObject<T>, Resource and Color

* Apply suggestions from code review

Co-authored-by: Pedro Jesus <[email protected]>

* Update AppThemeObjectOfT.shared.cs

* Moving some things around

* Add namespaces to URL namespace + fix sample

* `dotnet format`

* Update docs

* Enable string interpolation

* Remove unnecessary changes

* Use `static` class for improved performance

* Use real variable names

* Remove unnecessary `pragma`, add non-nullable default values

* Remove unnecessary condition

* Update AppThemeViewModel.cs

* Add theme toggle to sample

* Use ArgumentNullException.ThrowIfNull

* AppThemeExtension -> ThemeResourceExtension

* Update Description

* `dotnet format`

* AppTheme => Theme

* AppTheme => Theme

* Revert "AppTheme => Theme"

This reverts commit 7d77d02.

* Revert "AppTheme => Theme"

This reverts commit 1f46a73.

* Rename `ThemeResourceExtension` -> `AppThemeResourceExtension`

* Revert "Rename `ThemeResourceExtension` -> `AppThemeResourceExtension`"

This reverts commit 3b3d472.

* Rename `ThemeResourceExtension` -> `AppThemeResourceExtension` and `AppThemeResource` -> `AppThemeObject`

* Update AppThemeObjectExtensions.shared.cs

---------

Co-authored-by: Pedro Jesus <[email protected]>
Co-authored-by: Brandon Minnick <[email protected]>
  • Loading branch information
3 people authored Jul 27, 2023
1 parent 1bb29e0 commit c04539c
Show file tree
Hide file tree
Showing 13 changed files with 415 additions and 3 deletions.
1 change: 1 addition & 0 deletions samples/CommunityToolkit.Maui.Sample/AppShell.xaml.cs
Original file line number Diff line number Diff line change
Expand Up @@ -83,6 +83,7 @@ public partial class AppShell : Shell
CreateViewModelMapping<VariableMultiValueConverterPage, VariableMultiValueConverterViewModel, ConvertersGalleryPage, ConvertersGalleryViewModel>(),

// Add Essentials View Models
CreateViewModelMapping<AppThemePage, AppThemeViewModel, EssentialsGalleryPage, EssentialsGalleryViewModel>(),
CreateViewModelMapping<BadgePage, BadgeViewModel, EssentialsGalleryPage, EssentialsGalleryViewModel>(),
CreateViewModelMapping<FileSaverPage, FileSaverViewModel, EssentialsGalleryPage, EssentialsGalleryViewModel>(),
CreateViewModelMapping<FolderPickerPage, FolderPickerViewModel, EssentialsGalleryPage, EssentialsGalleryViewModel>(),
Expand Down
1 change: 1 addition & 0 deletions samples/CommunityToolkit.Maui.Sample/MauiProgram.cs
Original file line number Diff line number Diff line change
Expand Up @@ -157,6 +157,7 @@ static void RegisterViewsAndViewModels(in IServiceCollection services)
services.AddTransientWithShellRoute<VariableMultiValueConverterPage, VariableMultiValueConverterViewModel>();

// Add Essentials Pages + ViewModels
services.AddTransientWithShellRoute<AppThemePage, AppThemeViewModel>();
services.AddTransientWithShellRoute<BadgePage, BadgeViewModel>();
services.AddTransientWithShellRoute<FileSaverPage, FileSaverViewModel>();
services.AddTransientWithShellRoute<FolderPickerPage, FolderPickerViewModel>();
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
<?xml version="1.0" encoding="utf-8" ?>
<pages:BasePage xmlns="http://schemas.microsoft.com/dotnet/2021/maui"
xmlns:pages="clr-namespace:CommunityToolkit.Maui.Sample.Pages"
xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
x:Class="CommunityToolkit.Maui.Sample.Pages.Essentials.AppThemePage"
xmlns:mct="http://schemas.microsoft.com/dotnet/2022/maui/toolkit"
xmlns:vm="clr-namespace:CommunityToolkit.Maui.Sample.ViewModels.Essentials"
x:TypeArguments="vm:AppThemeViewModel"
x:DataType="vm:AppThemeViewModel"
Title="AppTheme">
<pages:BasePage.Resources>
<mct:AppThemeColor Light="Green" Dark="Red" x:Key="LabelTextColor" />
<mct:AppThemeObject Light="dotnet_bot.png" Dark="avatar_icon.png" x:Key="ImageSource" />

<Style x:Key="Headline" TargetType="Label">
<Setter Property="FontFamily" Value="Segoe UI" />
<Setter Property="FontSize" Value="10" />
<Setter Property="TextColor" Value="{mct:AppThemeResource LabelTextColor}" />
</Style>
</pages:BasePage.Resources>

<VerticalStackLayout Spacing="14">

<Label Text="AppTheme provides extension methods and markup extensions that make it easy to assign Light Theme, Dark Theme and Default Theme"
HorizontalTextAlignment="Center"
VerticalOptions="Center"/>

<Label
Text="This color comes from the resource dictionary!"
FontSize="20"
VerticalOptions="Center"
HorizontalOptions="Center"
TextColor="{mct:AppThemeResource LabelTextColor}"/>

<Label
Text="The below image comes from the resource dictionary!"
VerticalOptions="Center"
HorizontalOptions="Center"/>

<Image
WidthRequest="100"
HeightRequest="100"
VerticalOptions="Center"
HorizontalOptions="Center"
Source="{mct:AppThemeResource ImageSource}"/>

<Label
Text="This color comes from a style!"
VerticalOptions="Center"
HorizontalOptions="Center"
Style="{StaticResource Headline}"/>

<HorizontalStackLayout Spacing="5" HorizontalOptions="Center" Margin="0,0,0,20">
<Switch x:Name="themeToggle" Toggled="Switch_Toggled" />
<Label Text="Toggle Dark/Light Theme" VerticalOptions="Center" />
</HorizontalStackLayout>
</VerticalStackLayout>
</pages:BasePage>
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
using CommunityToolkit.Maui.Sample.ViewModels.Essentials;

namespace CommunityToolkit.Maui.Sample.Pages.Essentials;

public partial class AppThemePage : BasePage<AppThemeViewModel>
{
public AppThemePage(AppThemeViewModel viewModel) : base(viewModel)
{
InitializeComponent();
}

void Switch_Toggled(object sender, ToggledEventArgs e)
{
if (Application.Current is not null)
{
Application.Current.UserAppTheme = Application.Current.RequestedTheme is AppTheme.Dark
? AppTheme.Light
: AppTheme.Dark;
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
namespace CommunityToolkit.Maui.Sample.ViewModels.Essentials;

public class AppThemeViewModel : BaseViewModel
{
public AppThemeViewModel() : base()
{
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ public class EssentialsGalleryViewModel : BaseGalleryViewModel
public EssentialsGalleryViewModel()
: base(new[]
{
SectionModel.Create<AppThemeViewModel>("AppThemeResource", "AppThemeResource provides extension methods and markup extensions that make it easy to assign Light Theme, Dark Theme and Default Theme"),
SectionModel.Create<BadgeViewModel>("Badge", "Allows the user to set app icon badge count on the home screen"),
SectionModel.Create<FileSaverViewModel>("FileSaver", "Allows the user to save files to the filesystem"),
SectionModel.Create<FolderPickerViewModel>("FolderPicker", "Allows picking folders from the file system"),
Expand Down
119 changes: 119 additions & 0 deletions src/CommunityToolkit.Maui.UnitTests/Essentials/AppThemeTests.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,119 @@
using CommunityToolkit.Maui.Extensions;
using CommunityToolkit.Maui.UnitTests.Mocks;
using Xunit;

namespace CommunityToolkit.Maui.UnitTests.Essentials;

public class AppThemeTests : BaseTest
{
readonly MockAppInfo mockAppInfo;
readonly Application app;

public AppThemeTests()
{
AppInfo.SetCurrent(mockAppInfo = new() { RequestedTheme = AppTheme.Light });
Application.Current = app = new Application();
}

[Fact]
public void AppThemeColorUsesCorrectColorForTheme()
{
AppThemeColor color = new()
{
Light = Colors.Green,
Dark = Colors.Red
};

Label label = new()
{
Text = "Green on Light, Red on Dark"
};

label.SetAppThemeColor(Label.TextColorProperty, color);

Application.Current = null;

Assert.Equal(Colors.Green, label.TextColor);

SetAppTheme(AppTheme.Dark);

Assert.Equal(Colors.Red, label.TextColor);
}

[Fact]
public void AppThemeColorUsesDefaultColorWhenDarkColorNotSet()
{
AppThemeColor color = new()
{
Light = Colors.Green,
Default = Colors.Blue
};

Label label = new()
{
Text = "Green on Light, Red on Dark"
};

label.SetAppThemeColor(Label.TextColorProperty, color);

Application.Current = null;

Assert.Equal(Colors.Green, label.TextColor);

SetAppTheme(AppTheme.Dark);

Assert.Equal(Colors.Blue, label.TextColor);
}

[Fact]
public void AppThemeColorUsesDefaultColorWhenLightColorNotSet()
{
AppThemeColor color = new()
{
Default = Colors.Blue,
Dark = Colors.Red
};

Label label = new()
{
Text = "Green on Light, Red on Dark"
};

label.SetAppThemeColor(Label.TextColorProperty, color);

Application.Current = null;

Assert.Equal(Colors.Blue, label.TextColor);

SetAppTheme(AppTheme.Dark);

Assert.Equal(Colors.Red, label.TextColor);
}

[Fact]
public void AppThemeResourceUpdatesLabelText()
{
Label label = new();

AppThemeObject resource = new()
{
Light = "Light Theme",
Dark = "Dark Theme"
};

label.SetAppTheme(Label.TextProperty, resource);

Application.Current = null;
Assert.Equal("Light Theme", label.Text);

SetAppTheme(AppTheme.Dark);

Assert.Equal("Dark Theme", label.Text);
}

void SetAppTheme(AppTheme theme)
{
mockAppInfo.RequestedTheme = theme;
((IApplication)app).ThemeChanged();
}
}
24 changes: 24 additions & 0 deletions src/CommunityToolkit.Maui.UnitTests/Mocks/MockAppInfo.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
namespace CommunityToolkit.Maui.UnitTests.Mocks;

class MockAppInfo : IAppInfo
{
public string PackageName { get; set; } = string.Empty;

public string Name { get; set; } = string.Empty;

public string VersionString { get; set; } = string.Empty;

public string BuildString { get; set; } = string.Empty;

public Version Version { get; set; } = new Version(1, 0);

public LayoutDirection RequestedLayoutDirection { get; set; }

public AppTheme RequestedTheme { get; set; }

public AppPackagingModel PackagingModel { get; set; }

public void ShowSettingsUI()
{
}
}
8 changes: 5 additions & 3 deletions src/CommunityToolkit.Maui/AssemblyInfo.shared.cs
Original file line number Diff line number Diff line change
Expand Up @@ -2,15 +2,17 @@
[assembly: XmlnsDefinition(Constants.XamlNamespace, Constants.CommunityToolkitNamespacePrefix + nameof(CommunityToolkit.Maui.Alerts))]
[assembly: XmlnsDefinition(Constants.XamlNamespace, Constants.CommunityToolkitNamespacePrefix + nameof(CommunityToolkit.Maui.Behaviors))]
[assembly: XmlnsDefinition(Constants.XamlNamespace, Constants.CommunityToolkitNamespacePrefix + nameof(CommunityToolkit.Maui.Converters))]
[assembly: XmlnsDefinition(Constants.XamlNamespace, Constants.CommunityToolkitNamespacePrefix + nameof(CommunityToolkit.Maui.Extensions))]
[assembly: XmlnsDefinition(Constants.XamlNamespace, Constants.CommunityToolkitNamespacePrefix + nameof(CommunityToolkit.Maui.ImageSources))]
[assembly: XmlnsDefinition(Constants.XamlNamespace, Constants.CommunityToolkitNamespacePrefix + nameof(CommunityToolkit.Maui.Views))]
[assembly: XmlnsDefinition(Constants.XamlNamespace, Constants.CommunityToolkitNamespacePrefix + nameof(CommunityToolkit.Maui.Layouts))]
[assembly: XmlnsDefinition(Constants.XamlNamespace, Constants.CommunityToolkitNamespace)]

[assembly: Microsoft.Maui.Controls.XmlnsPrefix(Constants.XamlNamespace, "toolkit")]

class Constants
static class Constants
{
public const string XamlNamespace = "http://schemas.microsoft.com/dotnet/2022/maui/toolkit";

public const string CommunityToolkitNamespacePrefix = $"{nameof(CommunityToolkit)}.{nameof(CommunityToolkit.Maui)}.";
public const string CommunityToolkitNamespace = $"{nameof(CommunityToolkit)}.{nameof(CommunityToolkit.Maui)}";
public const string CommunityToolkitNamespacePrefix = $"{CommunityToolkitNamespace}.";
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
namespace CommunityToolkit.Maui;

/// <summary>
/// Represents a color that is aware of the operating system theme.
/// </summary>
public sealed class AppThemeColor : AppThemeObject<Color>
{
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
namespace CommunityToolkit.Maui;

/// <summary>
/// Represents a resource that is aware of the operating system theme.
/// </summary>
public sealed class AppThemeObject : AppThemeObject<object>
{
}

/// <summary>
/// Represents an object that is aware of the operating system theme.
/// </summary>
public abstract class AppThemeObject<T>
{
/// <summary>
/// The <see cref="object"/> that is used when the operating system uses light theme.
/// </summary>
public T? Light { get; set; }

/// <summary>
/// The <see cref="object"/> that is used when the operating system uses dark theme.
/// </summary>
public T? Dark { get; set; }

/// <summary>
/// The <see cref="object"/> that is used when the current theme is unspecified or
/// when a value is not provided for <see cref="Light"/> or <see cref="Dark"/>.
/// </summary>
public T? Default { get; set; }

/// <summary>
/// Gets a bindable object which holds the diffent values for each operating system theme.
/// </summary>
/// <returns>A <see cref="AppThemeBinding"/> instance with the respective theme values.</returns>
public virtual BindingBase GetBinding()
{
var binding = new AppThemeBinding();

if (Light is not null)
{
binding.Light = Light;
}

if (Dark is not null)
{
binding.Dark = Dark;
}

if (Default is not null)
{
binding.Default = Default;
}

return binding;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
namespace CommunityToolkit.Maui.Extensions;

/// <summary>
/// This class contains static extension methods for use with <see cref="BindableObject"/> objects.
/// </summary>
public static class AppThemeObjectExtensions
{
/// <summary>
/// Sets the <see cref="AppThemeColor"/> to the provided <see cref="BindableProperty"/> of the given <see cref="BindableObject"/>.
/// </summary>
/// <param name="self">The <see cref="BindableObject"/> on which the <paramref name="appThemeColor"/> will be applied to the provided property in <paramref name="targetProperty"/>.</param>
/// <param name="targetProperty">The <see cref="BindableProperty"/> on which to set the <paramref name="appThemeColor"/>.</param>
/// <param name="appThemeColor">The <see cref="AppThemeColor"/> to apply to <paramref name="targetProperty"/>.</param>
public static void SetAppThemeColor(this BindableObject self, BindableProperty targetProperty, AppThemeColor appThemeColor) =>
self.SetBinding(targetProperty, appThemeColor.GetBinding());

/// <summary>
/// Sets the <see cref="AppThemeObject"/> to the provided <see cref="BindableProperty"/> of the given <see cref="BindableObject"/>.
/// </summary>
/// <param name="self">The <see cref="BindableObject"/> on which the <paramref name="appThemeResource"/> will be applied to the provided property in <paramref name="targetProperty"/>.</param>
/// <param name="targetProperty">The <see cref="BindableProperty"/> on which to set the <paramref name="appThemeResource"/>.</param>
/// <param name="appThemeResource">The <see cref="AppThemeObject"/> to apply to <paramref name="targetProperty"/>.</param>
public static void SetAppTheme<T>(this BindableObject self, BindableProperty targetProperty, AppThemeObject<T> appThemeResource) =>
self.SetBinding(targetProperty, appThemeResource.GetBinding());
}
Loading

0 comments on commit c04539c

Please sign in to comment.