diff --git a/src/library/Uno.Material/MaterialResourcesV2.cs b/src/library/Uno.Material/MaterialResourcesV2.cs index 4be8f0175..84f1a63f0 100644 --- a/src/library/Uno.Material/MaterialResourcesV2.cs +++ b/src/library/Uno.Material/MaterialResourcesV2.cs @@ -107,9 +107,8 @@ private Style GetStyle(string key) Add("MaterialListViewItemStyle", isImplicit: true); Add("MaterialListViewStyle", isImplicit: true); Add("MaterialMenuFlyoutPresenterStyle", isImplicit: true); - Add("MaterialNavigationViewItemStyle", isImplicit: true); - Add("MaterialPaddedNavigationViewStyle"); Add("MaterialNavigationViewStyle", isImplicit: true); + Add("MaterialNavigationViewItemStyle", isImplicit: true); Add("MaterialOutlinedButtonStyle"); Add("MaterialOutlinedPasswordBoxStyle"); Add("MaterialOutlinedTextBoxStyle"); diff --git a/src/library/Uno.Material/Styles/Controls/v2/NavigationView.xaml b/src/library/Uno.Material/Styles/Controls/v2/NavigationView.xaml index aaf209ad7..ec2f4eda2 100644 --- a/src/library/Uno.Material/Styles/Controls/v2/NavigationView.xaml +++ b/src/library/Uno.Material/Styles/Controls/v2/NavigationView.xaml @@ -1,329 +1,199 @@  + mc:Ignorable="todo"> + - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + + 320 + 48 + 0,0,1,0 + 1,0,0,0 + + 00:00:00.2 + 00:00:00.19999 + 00:00:00.1 + 0 - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + + 320 + 48 + 0,0,1,0 + 1,0,0,0 + 00:00:00.2 + 00:00:00.19999 + 00:00:00.1 + 0 + + + 320 + 48 + 0,0,1,0 + 1,0,0,0 + + 00:00:00.2 + 00:00:00.19999 + 00:00:00.1 + 0 - 10,0,16,0 - 12,0,12,0 - 40 - 40 - 40 - 40 - 40 - 48 - 48 - 40 - - 40 - 4 - - 1 - 1 - - 1 - 1 - 12,0,12,0 - 10,0,0,0 - 8,5,0,0 - 12,5,0,11 - 8,4,0,0 - 0 - 0 - 16,10 - 16,10 - 10,0 - 0 - 0,0,20,0 - 0,0,0,0 - 8,0,16,0 - 12,0 - -20,0,6,0 - -16,0,0,0 - 0,0,0,0 - -12,0,0,0 - 12,0,20,0 - 16,0,20,0 - -4,0,6,0 - 12,0,12,0 + + + + + + 0,14,14,0 + 14,0,0,14 + + + 56 + 28 + 24 + 24 + 12,0 + + + + + + + + 56 + 28 + 28,0,0,28 + 12,0 + 24 + 12,0,0,0 + 0 + + + + + + + + + + 16,0 + 4,0 + 56 + 80 + 80 + 40 + 48 + 40 + 80 + 40 + 40 + 40 + 40 + + 56 + 56 + 24 + 3 + 24 + 2 + + 1 + 1 + + 0 + 1 + 3 + 12,0 + 16,0 + 12,0 + -24,44,0,0 + 0 + -1,3 + 1,1,0,0 + 0,1,0,0 + 0,1,0,0 + 4,0 + 1 + 56,44,0,0 + 0 + 0 + 0 + 0 + 8,4,0,0 + 0 + 0 + 0,3,0,4 + 0,3,0,4 + 3,0,4,0 + 0 + 4,-1,8,-1 + 0,0,0,0 + 8,-1,12,-1 + 12,0 + 0,0,-14,0 + -16,0,0,0 + 0,0,0,0 + -12,0,0,0 + 12,0,20,0 + 16,0,20,0 + -4,0,-8,0 + 12,0,12,0 + + 8,0,0,0 + 0 + 0 - 0,8 - 0,8 + 0,8 + 0,8 - 12.0 - - - - - - - - - - 0 - - - - 8,2 - 16 - + 8 + - - - - + + - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + win:Style="{StaticResource ResetDefaultSplitViewStyle}"> - - - + + + - - + + + - + - + + @@ -1274,82 +1243,130 @@ - + + + + + + + HorizontalContentAlignment="Stretch" + Grid.Row="4" /> - + - + + + + - - + + - - - - - + + + + + + - + - - + - + + HorizontalContentAlignment="Stretch" + Margin="0,0,0,4" + Grid.Row="2" /> - - + + - - - - - + VerticalScrollBarVisibility="Auto" + contract7Present:VerticalAnchorRatio="1"> + + + + + - + - + @@ -1360,35 +1377,43 @@ - - - + + Style="{StaticResource NavigationViewTitleHeaderContentControlTextStyle}" /> - + Content="{TemplateBinding Content}" + Margin="{ThemeResource NavigationViewContentPresenterMargin}" /> + + + + + + @@ -1397,7 +1422,6 @@ button Grid is moved below the content SplitView in the template --> @@ -1407,44 +1431,43 @@ - + - + + @@ -1456,835 +1479,510 @@ - + - - - - + + - + + + - - + + - - - - - - + - - - - - - + - - - - - + diff --git a/src/samples/Uno.Themes.Samples/Uno.Themes.Samples.Shared/App.xaml b/src/samples/Uno.Themes.Samples/Uno.Themes.Samples.Shared/App.xaml index c8f1dd14d..0983fd4fb 100644 --- a/src/samples/Uno.Themes.Samples/Uno.Themes.Samples.Shared/App.xaml +++ b/src/samples/Uno.Themes.Samples/Uno.Themes.Samples.Shared/App.xaml @@ -33,7 +33,6 @@ - diff --git a/src/samples/Uno.Themes.Samples/Uno.Themes.Samples.Shared/App.xaml.Navigation.cs b/src/samples/Uno.Themes.Samples/Uno.Themes.Samples.Shared/App.xaml.Navigation.cs index 203d0a556..8f3abcf04 100644 --- a/src/samples/Uno.Themes.Samples/Uno.Themes.Samples.Shared/App.xaml.Navigation.cs +++ b/src/samples/Uno.Themes.Samples/Uno.Themes.Samples.Shared/App.xaml.Navigation.cs @@ -23,6 +23,7 @@ using Windows.UI.Xaml.Controls.Primitives; using Windows.UI.Xaml.Data; using Windows.UI.Xaml.Input; +using Windows.UI.Xaml.Markup; using Windows.UI.Xaml.Media; using Windows.UI.Xaml.Navigation; using MUXC = Microsoft.UI.Xaml.Controls; @@ -77,7 +78,6 @@ private void ShellNavigateTo(Sample sample, bool trySynchronizeCurrentItem) } } - private Shell BuildShell() { _shell = new Shell(); @@ -121,51 +121,70 @@ private void AddNavigationItems(MUXC.NavigationView nv) .ThenBy(x => x.Title) .GroupBy(x => x.Category); + /* the menu items are grouped by their [SamplePage].Category, some items can be "ungrouped" (category=None): + * - {any item where category=none}... + * - Category xyz + * - {any item with category=xyz}... */ + foreach (var category in categories.OrderBy(x => x.Key)) { - var tier = 1; - + // create parent menu item for this category var parentItem = default(MUXC.NavigationViewItem); if (category.Key != SampleCategory.None) { parentItem = new MUXC.NavigationViewItem { Content = category.Key.GetDescription() ?? category.Key.ToString(), + Icon = CreateIconElement(GetCategoryIconSource()), SelectsOnInvoked = false, - Style = (Style)Resources[$"T{tier++}NavigationViewItemStyle"] - }.Apply(NavViewItemVisualStateFix); + }; AutomationProperties.SetAutomationId(parentItem, "Section_" + parentItem.Content); nv.MenuItems.Add(parentItem); + + object GetCategoryIconSource() + { + switch (category.Key) + { + case SampleCategory.Styles: return Icons.Styles.CategoryHeader; + case SampleCategory.Controls: return Icons.Controls.CategoryHeader; + case SampleCategory.Helpers: return Icons.Helpers.CategoryHeader; + + default: return Symbol.Placeholder; + } + } } + // add items to the parent menu item or directly to the nav-view foreach (var sample in category) { var item = new MUXC.NavigationViewItem { Content = sample.Title, + Icon = CreateIconElement(sample.IconSource), DataContext = sample, - Style = (Style)Resources[$"T{tier}NavigationViewItemStyle"] - }.Apply(NavViewItemVisualStateFix); + }; AutomationProperties.SetAutomationId(item, "Section_" + item.Content); (parentItem?.MenuItems ?? nv.MenuItems).Add(item); } } - void NavViewItemVisualStateFix(MUXC.NavigationViewItem nvi) + IconElement CreateIconElement(object source) { - // gallery#107: on uwp and uno, deselecting a NVI by selecting another NVI will leave the former in the "Selected" state - // to workaround this, we force reset the visual state when IsSelected becomes false - nvi.RegisterPropertyChangedCallback(MUXC.NavigationViewItemBase.IsSelectedProperty, (s, e) => + if (source is string path) { - if (!nvi.IsSelected) + return new PathIcon() { - // depending on the DisplayMode, a NVIP may or may not be used. - var nvip = VisualTreeHelperEx.GetFirstDescendant(nvi, x => x.Name == "NavigationViewItemPresenter"); - VisualStateManager.GoToState((Control)nvip ?? nvi, "Normal", true); - } - }); + Data = (Geometry)XamlBindingHelper.ConvertValue(typeof(Geometry), path) + }; + } + if (source is Symbol symbol && symbol != default) + { + return new SymbolIcon(symbol); + } + + return new SymbolIcon(Symbol.Placeholder); } } } diff --git a/src/samples/Uno.Themes.Samples/Uno.Themes.Samples.Shared/Content/Extensions/ControlExtensionsSamplePage.xaml.cs b/src/samples/Uno.Themes.Samples/Uno.Themes.Samples.Shared/Content/Extensions/ControlExtensionsSamplePage.xaml.cs index 653f741d8..1693d32c7 100644 --- a/src/samples/Uno.Themes.Samples/Uno.Themes.Samples.Shared/Content/Extensions/ControlExtensionsSamplePage.xaml.cs +++ b/src/samples/Uno.Themes.Samples/Uno.Themes.Samples.Shared/Content/Extensions/ControlExtensionsSamplePage.xaml.cs @@ -16,7 +16,7 @@ namespace Uno.Themes.Samples.Shared.Content.Extensions { - [SamplePage(SampleCategory.Helpers, "ControlExtensions")] + [SamplePage(SampleCategory.Helpers, "ControlExtensions", IconPath = Icons.Helpers.ControlExtensions)] public sealed partial class ControlExtensionsSamplePage : Page { public ControlExtensionsSamplePage() diff --git a/src/samples/Uno.Themes.Samples/Uno.Themes.Samples.Shared/Content/OverviewPage.xaml.cs b/src/samples/Uno.Themes.Samples/Uno.Themes.Samples.Shared/Content/OverviewPage.xaml.cs index a27b94a55..34649fa18 100644 --- a/src/samples/Uno.Themes.Samples/Uno.Themes.Samples.Shared/Content/OverviewPage.xaml.cs +++ b/src/samples/Uno.Themes.Samples/Uno.Themes.Samples.Shared/Content/OverviewPage.xaml.cs @@ -18,7 +18,7 @@ namespace Uno.Themes.Samples.Content { - [SamplePage(SampleCategory.None, "Overview")] + [SamplePage(SampleCategory.None, "Overview", IconPath = Icons.Overview)] public sealed partial class OverviewPage : Page { public OverviewPage() diff --git a/src/samples/Uno.Themes.Samples/Uno.Themes.Samples.Shared/Content/Styles/ColorsSamplePage.xaml.cs b/src/samples/Uno.Themes.Samples/Uno.Themes.Samples.Shared/Content/Styles/ColorsSamplePage.xaml.cs index a04012d5f..485dc5a40 100644 --- a/src/samples/Uno.Themes.Samples/Uno.Themes.Samples.Shared/Content/Styles/ColorsSamplePage.xaml.cs +++ b/src/samples/Uno.Themes.Samples/Uno.Themes.Samples.Shared/Content/Styles/ColorsSamplePage.xaml.cs @@ -19,7 +19,7 @@ namespace Uno.Themes.Samples.Content.Styles { - [SamplePage(SampleCategory.Styles, "Colors")] + [SamplePage(SampleCategory.Styles, "Colors", IconPath = Icons.Styles.Colors)] public sealed partial class ColorsSamplePage : Page { public ColorsSamplePage() diff --git a/src/samples/Uno.Themes.Samples/Uno.Themes.Samples.Shared/Controls/SamplePageLayout.cs b/src/samples/Uno.Themes.Samples/Uno.Themes.Samples.Shared/Controls/SamplePageLayout.cs index 6420b8f3d..cdd2cdf5a 100644 --- a/src/samples/Uno.Themes.Samples/Uno.Themes.Samples.Shared/Controls/SamplePageLayout.cs +++ b/src/samples/Uno.Themes.Samples/Uno.Themes.Samples.Shared/Controls/SamplePageLayout.cs @@ -181,7 +181,7 @@ private double GetRelativeOffset() { #if NETFX_CORE // On UWP we can count on finding a ScrollContentPresenter. - var scp = VisualTreeHelperEx.GetFirstDescendant(_scrollViewer); + var scp = VisualTreeHelperEx.FindFirstDescendant(_scrollViewer); var content = scp?.Content as FrameworkElement; var transform = _scrollingTabs.TransformToVisual(content); return transform.TransformPoint(new Point(0, 0)).Y - _scrollViewer.VerticalOffset; @@ -207,7 +207,7 @@ public T GetSampleChild(Design mode, string name) { var presenter = (ContentPresenter)GetTemplateChild($"{mode}ContentPresenter"); - return VisualTreeHelperEx.GetFirstDescendant(presenter, x => x.Name == name); + return VisualTreeHelperEx.FindFirstDescendant(presenter, x => x.Name == name); } private class LayoutModeMapping diff --git a/src/samples/Uno.Themes.Samples/Uno.Themes.Samples.Shared/Entities/Icons.cs b/src/samples/Uno.Themes.Samples/Uno.Themes.Samples.Shared/Entities/Icons.cs new file mode 100644 index 000000000..e04ae21ea --- /dev/null +++ b/src/samples/Uno.Themes.Samples/Uno.Themes.Samples.Shared/Entities/Icons.cs @@ -0,0 +1,32 @@ +using System; +using System.Collections.Generic; +using System.Text; +using Windows.UI.Xaml.Controls; + +namespace Uno.Themes.Samples.Entities +{ + public static class Icons + { + // sourced from: https://materialdesignicons.com/ + + public const string Overview = /* format-list-bulleted-type */ "M5,9.5L7.5,14H2.5L5,9.5M3,4H7V8H3V4M5,20A2,2 0 0,0 7,18A2,2 0 0,0 5,16A2,2 0 0,0 3,18A2,2 0 0,0 5,20M9,5V7H21V5H9M9,19H21V17H9V19M9,13H21V11H9V13Z"; + + public static class Styles + { + public const string CategoryHeader = /* palette-swatch-outline */ "M2.5 19.6L3.8 20.2V11.2L1.4 17C1 18.1 1.5 19.2 2.5 19.6M15.2 4.8L20.2 16.8L12.9 19.8L7.9 7.9V7.8L15.2 4.8M15.3 2.8C15 2.8 14.8 2.8 14.5 2.9L7.1 6C6.4 6.3 5.9 7 5.9 7.8C5.9 8 5.9 8.3 6 8.6L11 20.5C11.3 21.3 12 21.7 12.8 21.7C13.1 21.7 13.3 21.7 13.6 21.6L21 18.5C22 18.1 22.5 16.9 22.1 15.9L17.1 4C16.8 3.2 16 2.8 15.3 2.8M10.5 9.9C9.9 9.9 9.5 9.5 9.5 8.9S9.9 7.9 10.5 7.9C11.1 7.9 11.5 8.4 11.5 8.9S11.1 9.9 10.5 9.9M5.9 19.8C5.9 20.9 6.8 21.8 7.9 21.8H9.3L5.9 13.5V19.8Z"; + public const string Colors = /* palette-outline */ "M12,22A10,10 0 0,1 2,12A10,10 0 0,1 12,2C17.5,2 22,6 22,11A6,6 0 0,1 16,17H14.2C13.9,17 13.7,17.2 13.7,17.5C13.7,17.6 13.8,17.7 13.8,17.8C14.2,18.3 14.4,18.9 14.4,19.5C14.5,20.9 13.4,22 12,22M12,4A8,8 0 0,0 4,12A8,8 0 0,0 12,20C12.3,20 12.5,19.8 12.5,19.5C12.5,19.3 12.4,19.2 12.4,19.1C12,18.6 11.8,18.1 11.8,17.5C11.8,16.1 12.9,15 14.3,15H16A4,4 0 0,0 20,11C20,7.1 16.4,4 12,4M6.5,10C7.3,10 8,10.7 8,11.5C8,12.3 7.3,13 6.5,13C5.7,13 5,12.3 5,11.5C5,10.7 5.7,10 6.5,10M9.5,6C10.3,6 11,6.7 11,7.5C11,8.3 10.3,9 9.5,9C8.7,9 8,8.3 8,7.5C8,6.7 8.7,6 9.5,6M14.5,6C15.3,6 16,6.7 16,7.5C16,8.3 15.3,9 14.5,9C13.7,9 13,8.3 13,7.5C13,6.7 13.7,6 14.5,6M17.5,10C18.3,10 19,10.7 19,11.5C19,12.3 18.3,13 17.5,13C16.7,13 16,12.3 16,11.5C16,10.7 16.7,10 17.5,10Z"; + } + + public static class Controls + { + public const string CategoryHeader = /* rhombus-split */ "M12 2C11.5 2 11 2.19 10.59 2.59L7.29 5.88L12 10.58L16.71 5.88L13.41 2.59C13 2.19 12.5 2 12 2M5.88 7.29L2.59 10.59C1.8 11.37 1.8 12.63 2.59 13.41L5.88 16.71L10.58 12L5.88 7.29M18.12 7.29L13.42 12L18.12 16.71L21.41 13.41C22.2 12.63 22.2 11.37 21.41 10.59L18.12 7.29M12 13.42L7.29 18.12L10.59 21.41C11.37 22.2 12.63 22.2 13.41 21.41L16.71 18.12L12 13.42Z"; + public const Symbol Control = Symbol.Placeholder; + } + + public static class Helpers + { + public const string CategoryHeader = /* toolbox-outline */ "M20 8H17V6C17 4.9 16.1 4 15 4H9C7.9 4 7 4.9 7 6V8H4C2.9 8 2 8.9 2 10V20H22V10C22 8.9 21.1 8 20 8M9 6H15V8H9V6M20 18H4V15H6V16H8V15H16V16H18V15H20V18M18 13V12H16V13H8V12H6V13H4V10H20V13H18Z"; + public const string ControlExtensions = /* wrench-cog-outline */ "M9 1.09V6H7V1.09C4.16 1.57 2 4.03 2 7C2 9.22 3.21 11.15 5 12.19V21C5 21.55 5.45 22 6 22H10C10.55 22 11 21.55 11 21V12.19C12.79 11.15 14 9.22 14 7C14 4.03 11.84 1.57 9 1.09M10 10.46L9 11.04V20H7V11.04L6 10.46C4.77 9.74 4 8.42 4 7C4 6 4.37 5.06 5 4.35V8H11V4.35C11.63 5.06 12 6 12 7C12 8.42 11.23 9.74 10 10.46M20.94 17.94C20.96 17.79 20.97 17.65 20.97 17.5S20.96 17.2 20.94 17.05L21.91 16.32C22 16.25 22.03 16.13 21.97 16.03L21.05 14.47C21 14.37 20.87 14.33 20.76 14.37L19.61 14.82C19.37 14.65 19.12 14.5 18.83 14.38L18.66 13.19C18.64 13.08 18.54 13 18.43 13H16.58C16.46 13 16.36 13.08 16.34 13.19L16.17 14.38C15.88 14.5 15.63 14.65 15.39 14.82L14.24 14.37C14.14 14.33 14 14.37 13.96 14.47L13.03 16.03C12.97 16.13 13 16.25 13.09 16.32L14.06 17.05C14.05 17.2 14.03 17.35 14.03 17.5S14.05 17.79 14.06 17.94L13.09 18.68C13 18.75 12.97 18.87 13.03 18.97L13.96 20.53C14 20.63 14.14 20.66 14.24 20.63L15.39 20.17C15.63 20.35 15.88 20.5 16.17 20.62L16.34 21.81C16.36 21.92 16.46 22 16.58 22H18.43C18.54 22 18.64 21.92 18.66 21.81L18.83 20.62C19.12 20.5 19.37 20.35 19.61 20.17L20.76 20.63C20.87 20.66 21 20.63 21.05 20.53L21.97 18.97C22.03 18.87 22 18.75 21.91 18.68L20.94 17.94M17.5 19C16.67 19 16 18.33 16 17.5S16.67 16 17.5 16 19 16.67 19 17.5 18.33 19 17.5 19Z"; + } + } +} diff --git a/src/samples/Uno.Themes.Samples/Uno.Themes.Samples.Shared/Entities/Sample.cs b/src/samples/Uno.Themes.Samples/Uno.Themes.Samples.Shared/Entities/Sample.cs index 4128b19d5..cadfd59d1 100644 --- a/src/samples/Uno.Themes.Samples/Uno.Themes.Samples.Shared/Entities/Sample.cs +++ b/src/samples/Uno.Themes.Samples/Uno.Themes.Samples.Shared/Entities/Sample.cs @@ -3,6 +3,7 @@ using System.Text; using Uno.Extensions; using Uno.Logging; +using Windows.UI.Xaml.Controls; using Windows.UI.Xaml.Data; namespace Uno.Themes.Samples.Entities @@ -13,6 +14,7 @@ public class Sample public Sample(SamplePageAttribute attribute, Type viewType) { Category = attribute.Category; + IconSource = attribute.IconSymbol == default ? (object)attribute.IconPath : (object)attribute.IconSymbol; Title = attribute.Title; Description = attribute.Description; DocumentationLink = attribute.DocumentationLink; @@ -38,7 +40,9 @@ private object CreateData(Type dataType) } } - public SampleCategory Category { get; set; } + public SampleCategory Category { get; } + + public object IconSource { get; } public string Title { get; } diff --git a/src/samples/Uno.Themes.Samples/Uno.Themes.Samples.Shared/Entities/SamplePageAttribute.cs b/src/samples/Uno.Themes.Samples/Uno.Themes.Samples.Shared/Entities/SamplePageAttribute.cs index 9f1ffc9bc..4f302f43f 100644 --- a/src/samples/Uno.Themes.Samples/Uno.Themes.Samples.Shared/Entities/SamplePageAttribute.cs +++ b/src/samples/Uno.Themes.Samples/Uno.Themes.Samples.Shared/Entities/SamplePageAttribute.cs @@ -1,6 +1,7 @@ using System; using System.Collections.Generic; using System.Text; +using Windows.UI.Xaml.Controls; namespace Uno.Themes.Samples.Entities { @@ -19,6 +20,14 @@ public SamplePageAttribute(SampleCategory category, string title, SourceSdk sour /// public SampleCategory Category { get; } + /// + /// Symbol will take precedence over Path if specified. + /// Attribute property can only be primitive value, nullable not included. So 'default' is used in lieu. + /// + public Symbol IconSymbol { get; set; } = default; + + public string IconPath { get; set; } + public string Title { get; } public string Description { get; set; } diff --git a/src/samples/Uno.Themes.Samples/Uno.Themes.Samples.Shared/Helpers/VisualTreeHelperEx.cs b/src/samples/Uno.Themes.Samples/Uno.Themes.Samples.Shared/Helpers/VisualTreeHelperEx.cs index f6b5104c2..df74c58ef 100644 --- a/src/samples/Uno.Themes.Samples/Uno.Themes.Samples.Shared/Helpers/VisualTreeHelperEx.cs +++ b/src/samples/Uno.Themes.Samples/Uno.Themes.Samples.Shared/Helpers/VisualTreeHelperEx.cs @@ -1,39 +1,344 @@ using System; using System.Collections.Generic; using System.Linq; +using System.Reflection; using System.Text; using Windows.UI.Xaml; +using Windows.UI.Xaml.Controls; using Windows.UI.Xaml.Media; +#if __IOS__ +using UIKit; +using _View = UIKit.UIView; +#elif __MACOS__ +using AppKit; +using _View = AppKit.NSView; +#elif __ANDROID__ +using _View = Android.Views.View; +#else +using _View = Windows.UI.Xaml.DependencyObject; +#endif + namespace Uno.Themes.Samples.Helpers { - public static class VisualTreeHelperEx + public static partial class VisualTreeHelperEx { - public static T GetFirstDescendant(DependencyObject reference) => GetDescendants(reference) + /// + /// Returns the first ancestor of a specified type. + /// + /// The type of ancestor to find. + /// Any node of the visual tree + /// First Counting from the and not from the root of tree. + public static T FindFirstAncestor(this _View reference) => EnumerateAncestors(reference) + .OfType() + .FirstOrDefault(); + + /// + /// Returns the first ancestor of a specified type that satisfies the . + /// + /// The type of ancestor to find. + /// Any node of the visual tree + /// A function to test each node for a condition. + /// First Counting from the and not from the root of tree. + public static T FindFirstAncestor(this _View reference, Func predicate) => EnumerateAncestors(reference) + .OfType() + .FirstOrDefault(predicate); + + /// + /// Returns the first descendant of a specified type. + /// + /// The type of descendant to find. + /// Any node of the visual tree + /// The crawling is done depth first. + public static T FindFirstDescendant(this _View reference) => EnumerateDescendants(reference) .OfType() .FirstOrDefault(); - public static T GetFirstDescendant(DependencyObject reference, Func predicate) => GetDescendants(reference) + /// + /// Returns the first descendant of a specified type that satisfies the . + /// + /// The type of descendant to find. + /// Any node of the visual tree + /// A function to test each node for a condition. + /// The crawling is done depth first. + public static T FindFirstDescendant(this _View reference, Func predicate) => EnumerateDescendants(reference) + .OfType() + .FirstOrDefault(predicate); + + /// + /// Returns the first descendant of a specified type that satisfies the whose ancestors (up to ) and itself satisfy the . + /// + /// The type of descendant to find. + /// Any node of the visual tree + /// A function to test each ancestor for a condition. + /// A function to test each descendant for a condition. + /// The crawling is done depth first. + public static T FindFirstDescendant(this _View reference, Func<_View, bool> hierarchyPredicate, Func predicate) => EnumerateDescendants(reference, hierarchyPredicate) .OfType() .FirstOrDefault(predicate); - public static IEnumerable GetDescendants(DependencyObject reference) + /// + /// Returns all the visual descendants of a node. + /// + /// Any node of the visual tree + public static IEnumerable<_View> EnumerateDescendants(this _View reference) => EnumerateDescendants(reference, x => true); + + /// + /// Returns all the visual descendants of a node that satisfies the . + /// + /// Any node of the visual tree + /// + /// + public static IEnumerable<_View> EnumerateDescendants(this _View reference, Func<_View, bool> hierarchyPredicate) { - foreach (var child in GetChildren(reference)) + foreach (var child in reference.EnumerateChildren().Where(hierarchyPredicate)) { yield return child; - foreach (var grandchild in GetDescendants(child)) + foreach (var grandchild in child.EnumerateDescendants(hierarchyPredicate)) { yield return grandchild; } } } + + + // note: methods for retrieving children/ancestors exist with varying signatures. + // re-implementing them with unified & more inclusive signature for convenience. +#if __IOS__ || __MACOS__ + private static IEnumerable<_View> EnumerateAncestors(this _View o) + { + if (o is null) yield break; + while (o.Superview is _View parent) + { + yield return o = parent; + } + } + + private static IEnumerable<_View> EnumerateChildren(this _View o) + { + if (o is null) return Enumerable.Empty<_View>(); + return o.Subviews; + } +#elif __ANDROID__ + private static IEnumerable<_View> EnumerateAncestors(this _View o) + { + if (o is null) yield break; + + while (o.Parent is _View parent) + { + yield return o = parent; + } + } - public static IEnumerable GetChildren(DependencyObject reference) + private static IEnumerable<_View> EnumerateChildren(this _View reference) + { + if (reference is Android.Views.ViewGroup vg) + { + return Enumerable + .Range(0, vg.ChildCount) + .Select(vg.GetChildAt) + .Where(x => x != null).Cast<_View>(); + } + + return Enumerable.Empty<_View>(); + } +#else + private static IEnumerable<_View> EnumerateAncestors(this _View o) + { + if (o is null) yield break; + while (VisualTreeHelper.GetParent(o) is DependencyObject parent) + { + yield return o = parent; + } + } + + private static IEnumerable<_View> EnumerateChildren(this _View reference) { return Enumerable .Range(0, VisualTreeHelper.GetChildrenCount(reference)) .Select(x => VisualTreeHelper.GetChild(reference, x)); } +#endif + + public static _View GetTemplateRoot(this Control control) + { + return EnumerateChildren(control).FirstOrDefault(); + } + } + public static partial class VisualTreeHelperEx // debugging + { + /// + /// Produces a text representation of the visual tree. + /// + /// Any node of the visual tree + public static string TreeGraph(this _View reference) => TreeGraph(reference, DescribeVTNode); + + /// + /// Produces a text representation of the visual tree, using the provided method of description. + /// + /// Any node of the visual tree + /// A function to describe a visual tree node in a single line. + /// + public static string TreeGraph(this _View reference, Func<_View, string> describe) + { + var buffer = new StringBuilder(); + try + { + Walk(reference); + } + catch (Exception e) + { + buffer.AppendLine(); + buffer.AppendLine("An error has occurred while walking the visual tree:"); + buffer.Append(e.ToString()); + } + return buffer.ToString(); + + void Walk(_View o, int depth = 0) + { + Print(o, depth); + foreach (var child in EnumerateChildren(o)) + { + Walk(child, depth + 1); + } + } + void Print(_View o, int depth) + { + buffer + .Append(new string(' ', depth * 4)) + .Append(describe(o)) + .AppendLine(); + } + } + + public static void MonitorVisualStates(this Control control, bool includeInitialStates, Action onStatesChanged) + { + var root = (FrameworkElement)control.GetTemplateRoot(); + if (root == null) + { + onStatesChanged("The control doesn't have a template root."); + return; + } + var vsgs = VisualStateManager.GetVisualStateGroups(root).ToArray(); + if (!vsgs.Any()) + { + onStatesChanged("The control template doesn't contain any VisualStateGroup."); + return; + } + + foreach (var vsg in vsgs) + { + vsg.CurrentStateChanged += (s, e) => DebugVStates(vsg.Name); + } + if (includeInitialStates) + { + DebugVStates(); + } + + void DebugVStates(string updatedGroupName = null) + { + var summary = string.Join("\n", vsgs + .Where(x => x.CurrentState != null) + .Select(x => $"{x.Name}: {x.CurrentState?.Name}") + ); + onStatesChanged(summary); + } + } + + private static string DescribeVTNode(object x) + { + return new StringBuilder() + .Append(x.GetType().Name) + .Append((x as FrameworkElement)?.Name is string xname && xname.Length > 0 ? $"#{xname}" : string.Empty) + .Append($" // {string.Join(", ", GetDetails())}") + .ToString(); + + bool TryGetDpValue(object owner, string property, out T value) + { + if (owner is DependencyObject @do && + owner.GetType().GetProperty($"{property}Property", BindingFlags.Public | BindingFlags.Static | BindingFlags.FlattenHierarchy)?.GetValue(null, null) is DependencyProperty dp) + { + value = (T)@do.GetValue(dp); + return true; + } + + value = default; + return false; + } + string FormatCornerRadius(CornerRadius cr) + { + // format: uniform, [left,top,right,bottom] + if (cr.TopLeft == cr.TopRight && cr.TopRight == cr.BottomRight && cr.BottomRight == cr.BottomLeft) return $"{cr.TopLeft}"; + return $"[{cr.TopLeft},{cr.TopRight},{cr.BottomRight},{cr.BottomLeft}]"; + } + string FormatThickness(Thickness thickness) + { + // format: uniform, [same-left-right,same-top-bottom], [left,top,right,bottom] + if (thickness.Left == thickness.Top && thickness.Top == thickness.Right && thickness.Right == thickness.Bottom) return $"{thickness.Left}"; + if (thickness.Left == thickness.Right && thickness.Top == thickness.Bottom) return $"[{thickness.Left},{thickness.Top}]"; + return $"[{thickness.Left},{thickness.Top},{thickness.Right},{thickness.Bottom}]"; + } + string FormatLengthRange(double min, double value, double max) + { + if (min == 0 && max == double.PositiveInfinity) return value.ToString(); + + return $"[{min},{value},{max}]"; + } + string FormatGridLength(GridLength value) + { + // format: A,*,2.5*,2.5 + if (value.IsAuto) return "A"; + if (value.IsStar) return value.Value == 1 ? "*" : (value.Value.ToString() + "*"); + return value.Value.ToString(); + } + IEnumerable GetDetails() + { + if (x is FrameworkElement fe) + { + yield return $"Actual={fe.ActualWidth}x{fe.ActualHeight}"; + if (fe.TransformToVisual(Window.Current.Content).TransformPoint(default) is var absPos) + { + yield return $"AbsPos={absPos.X},{absPos.Y}"; + } + yield return $"HV={fe.HorizontalAlignment}/{fe.VerticalAlignment}"; + } + if (x is Grid grid) + { + if (grid.ColumnDefinitions.Any()) yield return $"Columns='{string.Join(',', grid.ColumnDefinitions.Select(cd => FormatGridLength(cd.Width)))}'"; + if (grid.RowDefinitions.Any()) yield return $"Rows='{string.Join(',', grid.RowDefinitions.Select(rd => FormatGridLength(rd.Height)))}'"; + } + /*if (x is FrameworkElement fe2 && + Grid.GetRow(fe2) is var row && + Grid.GetRowSpan(fe2) is var rowSpan && + Grid.GetColumn(fe2) is var column && + (row > 0 || rowSpan > 1 || ...) + ) + { + yield return $"R{ + }*/ + if (TryGetDpValue(x, "Margin", out var margin)) yield return $"Margin={FormatThickness(margin)}"; + if (TryGetDpValue(x, "Padding", out var padding)) yield return $"Padding={FormatThickness(padding)}"; + if (TryGetDpValue(x, "CornerRadius", out var cr)) yield return $"CornerRadius={FormatCornerRadius(cr)}"; + if (TryGetDpValue(x, "BorderThickness", out var bt)) yield return $"BorderThickness={FormatThickness(bt)}"; + + if (TryGetDpValue(x, "MinWidth", out var minWidth) && + TryGetDpValue(x, "Width", out var width) && + TryGetDpValue(x, "MaxWidth", out var maxWidth) && + TryGetDpValue(x, "MinHeight", out var minHeight) && + TryGetDpValue(x, "Height", out var height) && + TryGetDpValue(x, "MaxHeight", out var maxHeight)) yield return $"Size={FormatLengthRange(minWidth, width, maxWidth)}x{FormatLengthRange(minHeight, height, maxHeight)}"; + + if (TryGetDpValue(x, "Opacity", out var opacity)) yield return $"Opacity={opacity}"; + if (TryGetDpValue(x, "Visibility", out var visibility)) yield return $"Visibility={visibility}"; + if (x is Control c && + c.GetTemplateRoot() is FrameworkElement templateRoot && + VisualStateManager.GetVisualStateGroups(templateRoot) is var vsgs && + vsgs.Count > 0) + { + //yield return "VisualStates=" + string.Join("|", vsgs.Where(y => y.CurrentState != null).Select(y => $"{y.Name}={y.CurrentState.Name}")); + yield return "VisualStates=" + string.Join("|", vsgs.Where(y => y.CurrentState != null).Select(y => y.CurrentState.Name)); + } + } + } } } diff --git a/src/samples/Uno.Themes.Samples/Uno.Themes.Samples.Shared/Shell.xaml b/src/samples/Uno.Themes.Samples/Uno.Themes.Samples.Shared/Shell.xaml index 6e8e4c7e3..7f69d9163 100644 --- a/src/samples/Uno.Themes.Samples/Uno.Themes.Samples.Shared/Shell.xaml +++ b/src/samples/Uno.Themes.Samples/Uno.Themes.Samples.Shared/Shell.xaml @@ -7,45 +7,33 @@ mc:Ignorable="xamarin"> - - - + + + - - - - - - - - - - - + Uno.Themes SampleApp + + + @@ -53,5 +41,16 @@ Grid.RowSpan="3" Visibility="Collapsed" xamarin:Style="{StaticResource NativeDefaultFrame}" /> + + + +