diff --git a/src/Controls/src/Core/BindableObjectExtensions.cs b/src/Controls/src/Core/BindableObjectExtensions.cs index 8467add5eb09..3adbbb5ed109 100644 --- a/src/Controls/src/Core/BindableObjectExtensions.cs +++ b/src/Controls/src/Core/BindableObjectExtensions.cs @@ -70,6 +70,45 @@ public static T GetPropertyIfSet(this BindableObject bindableObject, Bindable return returnIfNotSet; } + internal static bool TrySetDynamicThemeColor( + this BindableObject bindableObject, + string resourceKey, + BindableProperty bindableProperty, + out object outerColor) + { + if (Application.Current.TryGetResource(resourceKey, out outerColor)) + { + bindableObject.SetDynamicResource(bindableProperty, resourceKey); + return true; + } + + return false; + } + + internal static bool TrySetAppTheme( + this BindableObject self, + string lightResourceKey, + string darkResourceKey, + BindableProperty bindableProperty, + Brush defaultDark, + Brush defaultLight, + out object outerLight, + out object outerDark) + { + if (!Application.Current.TryGetResource(lightResourceKey, out outerLight)) + { + outerLight = defaultLight; + } + + if (!Application.Current.TryGetResource(darkResourceKey, out outerDark)) + { + outerDark = defaultDark; + } + + self.SetAppTheme(bindableProperty, outerLight, outerDark); + return (Brush)outerLight != defaultLight || (Brush)outerDark != defaultDark; + } + public static void SetAppTheme(this BindableObject self, BindableProperty targetProperty, T light, T dark) => self.SetBinding(targetProperty, new AppThemeBinding { Light = light, Dark = dark }); /// diff --git a/src/Controls/src/Core/HandlerImpl/RadioButton/RadioButton.Tizen.cs b/src/Controls/src/Core/HandlerImpl/RadioButton/RadioButton.Tizen.cs index 8e90cf9dced7..ab05132a7a1f 100644 --- a/src/Controls/src/Core/HandlerImpl/RadioButton/RadioButton.Tizen.cs +++ b/src/Controls/src/Core/HandlerImpl/RadioButton/RadioButton.Tizen.cs @@ -1,6 +1,7 @@ #nullable disable using Microsoft.Maui.Controls.Internals; using Microsoft.Maui.Controls.Shapes; +using Microsoft.Maui.Graphics; namespace Microsoft.Maui.Controls { @@ -22,20 +23,32 @@ public static void MapContent(IRadioButtonHandler handler, RadioButton radioButt static View BuildTizenDefaultTemplate() { - var frame = new Frame + Border border = new Border() { - HasShadow = false, Padding = 6 }; - BindToTemplatedParent(frame, BackgroundColorProperty, Controls.Frame.BorderColorProperty, Controls.Frame.CornerRadiusProperty, HorizontalOptionsProperty, + BindToTemplatedParent(border, BackgroundColorProperty, HorizontalOptionsProperty, MarginProperty, OpacityProperty, RotationProperty, ScaleProperty, ScaleXProperty, ScaleYProperty, TranslationYProperty, TranslationXProperty, VerticalOptionsProperty); + border.SetBinding(Border.StrokeProperty, + new Binding(BorderColorProperty.PropertyName, + source: RelativeBindingSource.TemplatedParent)); + + border.SetBinding(Border.StrokeShapeProperty, + new Binding(CornerRadiusProperty.PropertyName, converter: new CornerRadiusToShape(), + source: RelativeBindingSource.TemplatedParent)); + + border.SetBinding(Border.StrokeThicknessProperty, + new Binding(BorderWidthProperty.PropertyName, + source: RelativeBindingSource.TemplatedParent)); + var grid = new Grid { - ColumnSpacing = 6, + Padding = 2, RowSpacing = 0, + ColumnSpacing = 6, ColumnDefinitions = new ColumnDefinitionCollection { new ColumnDefinition { Width = GridLength.Auto }, new ColumnDefinition { Width = GridLength.Star } @@ -54,13 +67,11 @@ static View BuildTizenDefaultTemplate() HeightRequest = 21, WidthRequest = 21, StrokeThickness = 2, - Stroke = RadioButtonThemeColor, InputTransparent = true }; var checkMark = new Ellipse { - Fill = RadioButtonCheckMarkThemeColor, Aspect = Stretch.Uniform, HorizontalOptions = LayoutOptions.Center, VerticalOptions = LayoutOptions.Center, @@ -73,9 +84,62 @@ static View BuildTizenDefaultTemplate() var contentPresenter = new ContentPresenter { HorizontalOptions = LayoutOptions.Fill, - VerticalOptions = LayoutOptions.Center + VerticalOptions = LayoutOptions.Fill }; + object dynamicOuterEllipseThemeColor = null; + object dynamicCheckMarkThemeColor = null; + object outerEllipseVisualStateLight = null; + object outerEllipseVisualStateDark = null; + object checkMarkVisualStateLight = null; + object checkMarkVisualStateDark = null; + + if (!normalEllipse.TrySetDynamicThemeColor( + RadioButtonThemeColor, + Ellipse.StrokeProperty, + out dynamicOuterEllipseThemeColor)) + { + normalEllipse.TrySetAppTheme( + RadioButtonOuterEllipseStrokeLight, + RadioButtonOuterEllipseStrokeDark, + Ellipse.StrokeProperty, + SolidColorBrush.White, + SolidColorBrush.Black, + out outerEllipseVisualStateLight, + out outerEllipseVisualStateDark); + } + + if (!checkMark.TrySetDynamicThemeColor( + RadioButtonCheckMarkThemeColor, + Ellipse.StrokeProperty, + out dynamicCheckMarkThemeColor)) + { + checkMark.TrySetAppTheme( + RadioButtonCheckGlyphStrokeLight, + RadioButtonCheckGlyphStrokeDark, + Ellipse.StrokeProperty, + SolidColorBrush.White, + SolidColorBrush.Black, + out checkMarkVisualStateLight, + out checkMarkVisualStateDark); + } + + if (!checkMark.TrySetDynamicThemeColor( + RadioButtonCheckMarkThemeColor, + Ellipse.FillProperty, + out dynamicCheckMarkThemeColor)) + { + checkMark.TrySetAppTheme( + RadioButtonCheckGlyphFillLight, + RadioButtonCheckGlyphFillDark, + Ellipse.FillProperty, + SolidColorBrush.White, + SolidColorBrush.Black, + out _, + out _); + } + + contentPresenter.SetBinding(MarginProperty, new Binding("Padding", source: RelativeBindingSource.TemplatedParent)); contentPresenter.SetBinding(BackgroundColorProperty, new Binding(BackgroundColorProperty.PropertyName, source: RelativeBindingSource.TemplatedParent)); @@ -83,11 +147,11 @@ static View BuildTizenDefaultTemplate() grid.Add(checkMark); grid.Add(contentPresenter, 1, 0); - frame.Content = grid; + border.Content = grid; INameScope nameScope = new NameScope(); - NameScope.SetNameScope(frame, nameScope); - nameScope.RegisterName(TemplateRootName, frame); + NameScope.SetNameScope(border, nameScope); + nameScope.RegisterName(TemplateRootName, border); nameScope.RegisterName(UncheckedButton, normalEllipse); nameScope.RegisterName(CheckedIndicator, checkMark); nameScope.RegisterName("ContentPresenter", contentPresenter); @@ -104,19 +168,40 @@ static View BuildTizenDefaultTemplate() VisualState checkedVisualState = new VisualState() { Name = CheckedVisualState }; checkedVisualState.Setters.Add(new Setter() { Property = OpacityProperty, TargetName = CheckedIndicator, Value = 1 }); - checkedVisualState.Setters.Add(new Setter() { Property = Shape.StrokeProperty, TargetName = UncheckedButton, Value = RadioButtonCheckMarkThemeColor }); + checkedVisualState.Setters.Add( + new Setter() + { + Property = Shape.StrokeProperty, + TargetName = UncheckedButton, + Value = dynamicOuterEllipseThemeColor is not null ? dynamicOuterEllipseThemeColor : new AppThemeBinding() { Light = outerEllipseVisualStateLight, Dark = outerEllipseVisualStateDark } + }); + checkedVisualState.Setters.Add( + new Setter() + { + Property = Shape.StrokeProperty, + TargetName = CheckedIndicator, + Value = dynamicCheckMarkThemeColor is not null ? dynamicCheckMarkThemeColor : new AppThemeBinding() { Light = checkMarkVisualStateLight, Dark = checkMarkVisualStateDark } + }); checkedStates.States.Add(checkedVisualState); VisualState uncheckedVisualState = new VisualState() { Name = UncheckedVisualState }; uncheckedVisualState.Setters.Add(new Setter() { Property = OpacityProperty, TargetName = CheckedIndicator, Value = 0 }); - uncheckedVisualState.Setters.Add(new Setter() { Property = Shape.StrokeProperty, TargetName = UncheckedButton, Value = RadioButtonThemeColor }); + + uncheckedVisualState.Setters.Add( + new Setter() + { + Property = Shape.StrokeProperty, + TargetName = UncheckedButton, + Value = dynamicOuterEllipseThemeColor is not null ? dynamicOuterEllipseThemeColor : new AppThemeBinding() { Light = outerEllipseVisualStateLight, Dark = outerEllipseVisualStateDark } + }); + checkedStates.States.Add(uncheckedVisualState); visualStateGroups.Add(checkedStates); - VisualStateManager.SetVisualStateGroups(frame, visualStateGroups); + VisualStateManager.SetVisualStateGroups(border, visualStateGroups); - return frame; + return border; } } } \ No newline at end of file diff --git a/src/Controls/src/Core/RadioButton.cs b/src/Controls/src/Core/RadioButton.cs index a65444140fed..73c6a6701e8c 100644 --- a/src/Controls/src/Core/RadioButton.cs +++ b/src/Controls/src/Core/RadioButton.cs @@ -27,12 +27,23 @@ public partial class RadioButton : TemplatedView, IElementConfiguration> _platformConfigurationRegistry; @@ -333,21 +344,6 @@ void UpdateIsEnabled() } } - static Brush ResolveThemeColor(string key) - { - if (Application.Current.TryGetResource(key, out object color)) - { - return (Brush)color; - } - - if (Application.Current?.RequestedTheme == AppTheme.Dark) - { - return Brush.White; - } - - return Brush.Black; - } - void ApplyIsCheckedState() { if (IsChecked) @@ -504,13 +500,11 @@ static View BuildDefaultTemplate() HeightRequest = 21, WidthRequest = 21, StrokeThickness = 2, - Stroke = RadioButtonThemeColor, InputTransparent = true }; var checkMark = new Ellipse { - Fill = RadioButtonCheckMarkThemeColor, Aspect = Stretch.Uniform, HorizontalOptions = LayoutOptions.Center, VerticalOptions = LayoutOptions.Center, @@ -526,6 +520,58 @@ static View BuildDefaultTemplate() VerticalOptions = LayoutOptions.Fill }; + object dynamicOuterEllipseThemeColor = null; + object dynamicCheckMarkThemeColor = null; + object outerEllipseVisualStateLight = null; + object outerEllipseVisualStateDark = null; + object checkMarkVisualStateLight = null; + object checkMarkVisualStateDark = null; + + if (!normalEllipse.TrySetDynamicThemeColor( + RadioButtonThemeColor, + Ellipse.StrokeProperty, + out dynamicOuterEllipseThemeColor)) + { + normalEllipse.TrySetAppTheme( + RadioButtonOuterEllipseStrokeLight, + RadioButtonOuterEllipseStrokeDark, + Ellipse.StrokeProperty, + SolidColorBrush.White, + SolidColorBrush.Black, + out outerEllipseVisualStateLight, + out outerEllipseVisualStateDark); + } + + if (!checkMark.TrySetDynamicThemeColor( + RadioButtonCheckMarkThemeColor, + Ellipse.StrokeProperty, + out dynamicCheckMarkThemeColor)) + { + checkMark.TrySetAppTheme( + RadioButtonCheckGlyphStrokeLight, + RadioButtonCheckGlyphStrokeDark, + Ellipse.StrokeProperty, + SolidColorBrush.White, + SolidColorBrush.Black, + out checkMarkVisualStateLight, + out checkMarkVisualStateDark); + } + + if (!checkMark.TrySetDynamicThemeColor( + RadioButtonCheckMarkThemeColor, + Ellipse.FillProperty, + out dynamicCheckMarkThemeColor)) + { + checkMark.TrySetAppTheme( + RadioButtonCheckGlyphFillLight, + RadioButtonCheckGlyphFillDark, + Ellipse.FillProperty, + SolidColorBrush.White, + SolidColorBrush.Black, + out _, + out _); + } + contentPresenter.SetBinding(MarginProperty, new Binding("Padding", source: RelativeBindingSource.TemplatedParent)); contentPresenter.SetBinding(BackgroundColorProperty, new Binding(BackgroundColorProperty.PropertyName, source: RelativeBindingSource.TemplatedParent)); @@ -555,12 +601,33 @@ static View BuildDefaultTemplate() VisualState checkedVisualState = new VisualState() { Name = CheckedVisualState }; checkedVisualState.Setters.Add(new Setter() { Property = OpacityProperty, TargetName = CheckedIndicator, Value = 1 }); - checkedVisualState.Setters.Add(new Setter() { Property = Shape.StrokeProperty, TargetName = UncheckedButton, Value = RadioButtonCheckMarkThemeColor }); + checkedVisualState.Setters.Add( + new Setter() + { + Property = Shape.StrokeProperty, + TargetName = UncheckedButton, + Value = dynamicOuterEllipseThemeColor is not null ? dynamicOuterEllipseThemeColor : new AppThemeBinding() { Light = outerEllipseVisualStateLight, Dark = outerEllipseVisualStateDark } + }); + checkedVisualState.Setters.Add( + new Setter() + { + Property = Shape.StrokeProperty, + TargetName = CheckedIndicator, + Value = dynamicCheckMarkThemeColor is not null ? dynamicCheckMarkThemeColor : new AppThemeBinding() { Light = checkMarkVisualStateLight, Dark = checkMarkVisualStateDark } + }); checkedStates.States.Add(checkedVisualState); VisualState uncheckedVisualState = new VisualState() { Name = UncheckedVisualState }; uncheckedVisualState.Setters.Add(new Setter() { Property = OpacityProperty, TargetName = CheckedIndicator, Value = 0 }); - uncheckedVisualState.Setters.Add(new Setter() { Property = Shape.StrokeProperty, TargetName = UncheckedButton, Value = RadioButtonThemeColor }); + + uncheckedVisualState.Setters.Add( + new Setter() + { + Property = Shape.StrokeProperty, + TargetName = UncheckedButton, + Value = dynamicOuterEllipseThemeColor is not null ? dynamicOuterEllipseThemeColor : new AppThemeBinding() { Light = outerEllipseVisualStateLight, Dark = outerEllipseVisualStateDark } + }); + checkedStates.States.Add(uncheckedVisualState); visualStateGroups.Add(checkedStates); diff --git a/src/Controls/tests/Core.UnitTests/AppThemeTests.cs b/src/Controls/tests/Core.UnitTests/AppThemeTests.cs index 116012673794..f8e60f70dde4 100644 --- a/src/Controls/tests/Core.UnitTests/AppThemeTests.cs +++ b/src/Controls/tests/Core.UnitTests/AppThemeTests.cs @@ -195,6 +195,29 @@ public void ThemeBindingRemovedOnOneTimeBindablePropertyWhenPropertySet() Assert.Equal(Colors.Pink, shell.FlyoutBackgroundColor); } + void validateRadioButtonColors(RadioButton button, SolidColorBrush desiredBrush) + { + var border = (Border)button.Children[0]; + var grid = (Grid)border.Content; + var outerEllipse = (Shapes.Ellipse)grid.Children[0]; + var innerEllipse = (Shapes.Ellipse)grid.Children[1]; + + Assert.Equal(desiredBrush, outerEllipse.Stroke); + Assert.Equal(desiredBrush, innerEllipse.Fill); + } + + [Fact] + public void CorrectDefaultRadioButtonThemeColorsInLightAndDarkModes() + { + validateRadioButtonColors( + new RadioButton() { ControlTemplate = RadioButton.DefaultTemplate }, + Brush.Black); + SetAppTheme(AppTheme.Dark); + validateRadioButtonColors( + new RadioButton() { ControlTemplate = RadioButton.DefaultTemplate }, + Brush.White); + } + [Fact] public void NullApplicationCurrentFallsBackToEssentials() {