Skip to content

Ribbon #546

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

Merged
merged 9 commits into from
Jul 31, 2024
Merged
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions components/Ribbon/OpenSolution.bat
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
@ECHO OFF

powershell ..\..\tooling\ProjectHeads\GenerateSingleSampleHeads.ps1 -componentPath %CD% %*
Binary file added components/Ribbon/samples/Assets/Ribbon.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
31 changes: 31 additions & 0 deletions components/Ribbon/samples/Dependencies.props
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
<!--
WinUI 2 under UWP uses TargetFramework uap10.0.*
WinUI 3 under WinAppSdk uses TargetFramework net6.0-windows10.*
However, under Uno-powered platforms, both WinUI 2 and 3 can share the same TargetFramework.
MSBuild doesn't play nicely with this out of the box, so we've made it easy for you.
For .NET Standard packages, you can use the Nuget Package Manager in Visual Studio.
For UWP / WinAppSDK / Uno packages, place the package references here.
-->
<Project>
<!-- WinUI 2 / UWP -->
<ItemGroup Condition="'$(IsUwp)' == 'true'">
<!-- <PackageReference Include="Microsoft.Toolkit.Uwp.UI.Controls.Primitives" Version="7.1.2"/> -->
</ItemGroup>

<!-- WinUI 2 / Uno -->
<ItemGroup Condition="'$(IsUno)' == 'true' AND '$(WinUIMajorVersion)' == '2'">
<!-- <PackageReference Include="Uno.Microsoft.Toolkit.Uwp.UI.Controls.Primitives" Version="7.1.11"/> -->
</ItemGroup>

<!-- WinUI 3 / WinAppSdk -->
<ItemGroup Condition="'$(IsWinAppSdk)' == 'true'">
<!-- <PackageReference Include="CommunityToolkit.WinUI.UI.Controls.Primitives" Version="7.1.2"/> -->
</ItemGroup>

<!-- WinUI 3 / Uno -->
<ItemGroup Condition="'$(IsUno)' == 'true' AND '$(WinUIMajorVersion)' == '3'">
<!-- <PackageReference Include="Uno.CommunityToolkit.WinUI.UI.Controls.Primitives" Version="7.1.100-dev.15.g12261e2626"/> -->
</ItemGroup>
</Project>
18 changes: 18 additions & 0 deletions components/Ribbon/samples/Ribbon.Samples.csproj
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
<Project>
<Import Project="$([MSBuild]::GetPathOfFileAbove(Directory.Build.props))" Condition="Exists('$([MSBuild]::GetPathOfFileAbove(Directory.Build.props))')" />

<PropertyGroup>
<ToolkitComponentName>Ribbon</ToolkitComponentName>
</PropertyGroup>

<!-- Sets this up as a toolkit component's sample project -->
<Import Project="$(ToolingDirectory)\ToolkitComponent.SampleProject.props" />
<ItemGroup>
<None Remove="Assets\Ribbon.png" />
</ItemGroup>
<ItemGroup>
<Content Include="Assets\Ribbon.png">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</Content>
</ItemGroup>
</Project>
67 changes: 67 additions & 0 deletions components/Ribbon/samples/Ribbon.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
---
title: Ribbon
author: vgromfeld
description: An office like ribbon.
keywords: Ribbon, Control
dev_langs:
- csharp
category: Controls
subcategory: Layout
experimental: true
discussion-id: 544
issue-id: 545
icon: Assets/Ribbon.png
---

# Ribbon

An Office like Ribbon control which displays groups of commands. If there is not enough space to display all the groups,
some of them can be collapsed based on a priority order.

> [!Sample RibbonCustomSample]
## RibbonGroup

A basic group displayed in a Ribbon.
It mostly adds a *label* to some content and will not collapse if there is not enough space available.


## RibbonCollapsibleGroup

A `RibbonGroup` which can be collapsed if its content does not fit in the available Ribbon's space.
When collapsed, the group is displayed as a single icon button. Clicking on this button opens
a flyout containing the group's content.

### IconSource
The icon to display when the group is collapsed.

### AutoCloseFlyout
Set to true to automatically close the overflow flyout if one interactive element is clicked.
Note that the logic to detect the click is very basic. It will request the flyout to close
for all the handled pointer released events. It assumes that if the pointer has been handled
something reacted to it. It works well for buttons or check boxes but does not work for text
or combo boxes.

### Priority
The priority of the group.
The group with the lower priority will be the first one to be collapsed.

### CollapsedAccessKey
The access key to access the collapsed button and open the flyout when the group is collapsed.

### RequestedWidths

The list of requested widths for the group.
If null or empty, the group will automatically use the size of its content.
If set, the group will use the smallest provided width fitting in the ribbon.
This is useful if the group contains a variable size control which can adjust
its width (like a GridView with several items).

### State
The state of the group (collapsed or visible). This property is used by the `RibbonPanel`.

## RibbonPanel

The inner panel of the Ribbon control. It displays the groups inside the `Ribbon` and
automatically collapse the `CollapsibleGroup` elements based on their priority order if
there is not enough space available.
250 changes: 250 additions & 0 deletions components/Ribbon/samples/RibbonCustomSample.xaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,250 @@
<!-- Licensed to the .NET Foundation under one or more agreements. The .NET Foundation licenses this file to you under the MIT license. See the LICENSE file in the project root for more information. -->
<Page x:Class="RibbonExperiment.Samples.RibbonCustomSample"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:controls="using:CommunityToolkit.WinUI.Controls"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:local="using:RibbonExperiment.Samples"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
mc:Ignorable="d">

<!--
TODO: The generic.xaml file from the code project is not imported when the sample is loaded in the gallery app.
As a mitigation, we are manually importing the style dictionary here.
-->
<Page.Resources>
<ResourceDictionary Source="ms-appx:///CommunityToolkit.WinUI.Controls.Ribbon/RibbonStyle.xaml" />
</Page.Resources>
Comment on lines +15 to +17
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@Arlodotexe know what might be going on here, this seems weird as we haven't seen this with other controls?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The Generic.xaml file is present and working for the src project, but the Generic.xaml file isn't present in the samples project. It needs to be added.

Copy link
Member

@Arlodotexe Arlodotexe Jul 22, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Tried to implement these changes myself, didn't seem to help 🤔
No other component is using a Generic.xaml file in the sample project, it seems like something else is happening here. Investigating...

Copy link
Member

@Arlodotexe Arlodotexe Jul 22, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Interesting, all the existing xaml files (resource dictionaries, controls, etc) are backed by a cs file, except for this new one. We might be auto-importing backed xaml/cs files somewhere in our tooling, but not the xaml files alone. Still investigating.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Let's see how it works in a package and we can do a separate fix, if needed. Going to squash and merge this one.


<StackPanel Spacing="16">
<StackPanel.Resources>
<Style BasedOn="{StaticResource DefaultAppBarButtonStyle}"
TargetType="AppBarButton">
<Setter Property="LabelPosition" Value="Collapsed" />
<Setter Property="Width" Value="48" />
<Setter Property="Margin" Value="4" />
<Setter Property="Height" Value="48" />
</Style>
</StackPanel.Resources>

<controls:Ribbon HorizontalAlignment="Stretch">

<controls:RibbonCollapsibleGroup AccessKey="AB"
CollapsedAccessKey="AA"
Label="Edit"
Style="{StaticResource RibbonLeftCollapsibleGroupStyle}">
<controls:RibbonCollapsibleGroup.IconSource>
<SymbolIconSource Symbol="Add" />
</controls:RibbonCollapsibleGroup.IconSource>

<Grid>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="Auto" />
<ColumnDefinition Width="Auto" />
<ColumnDefinition Width="Auto" />
</Grid.ColumnDefinitions>
<Grid.RowDefinitions>
<RowDefinition Height="*" />
<RowDefinition Height="*" />
</Grid.RowDefinitions>

<AppBarButton Icon="Accept" />
<AppBarButton Grid.Column="1"
Icon="Account" />

<AppBarButton Grid.Row="1"
Icon="Add" />
<AppBarButton Grid.Row="1"
Grid.Column="1"
Icon="AddFriend" />
<AppBarButton Grid.RowSpan="2"
Grid.Column="2"
Icon="Admin" />
</Grid>
</controls:RibbonCollapsibleGroup>

<controls:RibbonCollapsibleGroup CollapsedAccessKey="B"
Label="Text"
Priority="1"
Style="{StaticResource RibbonLeftCollapsibleGroupStyle}">
<controls:RibbonCollapsibleGroup.IconSource>
<SymbolIconSource Symbol="Remove" />
</controls:RibbonCollapsibleGroup.IconSource>

<Grid>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="Auto" />
<ColumnDefinition Width="Auto" />
<ColumnDefinition Width="Auto" />
</Grid.ColumnDefinitions>
<Grid.RowDefinitions>
<RowDefinition Height="*" />
<RowDefinition Height="*" />
</Grid.RowDefinitions>

<AppBarButton Icon="AllApps" />
<AppBarButton Grid.Column="1"
Icon="Attach" />
<DropDownButton Grid.Column="2"
Content="Color">
<DropDownButton.Flyout>
<Flyout>
<Border Width="200"
Height="200"
Background="Red" />
</Flyout>
</DropDownButton.Flyout>
</DropDownButton>

<AppBarButton Grid.Row="1"
Icon="GoToStart" />
<AppBarButton Grid.Row="1"
Grid.Column="1"
Icon="GlobalNavigationButton" />
<AppBarButton Grid.Row="1"
Grid.Column="2"
Icon="ClosePane" />
</Grid>
</controls:RibbonCollapsibleGroup>

<controls:RibbonGroup Label="Color">
<Button Content="Pick color" />
</controls:RibbonGroup>

<controls:RibbonCollapsibleGroup CollapsedAccessKey="C"
Label="Text">
<controls:RibbonCollapsibleGroup.IconSource>
<SymbolIconSource Symbol="Font" />
</controls:RibbonCollapsibleGroup.IconSource>

<Grid>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="Auto" />
<ColumnDefinition Width="Auto" />
<ColumnDefinition Width="Auto" />
<ColumnDefinition Width="Auto" />
</Grid.ColumnDefinitions>
<Grid.RowDefinitions>
<RowDefinition Height="*" />
<RowDefinition Height="*" />
</Grid.RowDefinitions>

<AppBarButton Grid.RowSpan="2"
VerticalAlignment="Center"
Icon="Font" />
<AppBarButton Grid.Column="1"
Icon="AlignLeft" />
<AppBarButton Grid.Column="2"
Icon="AlignCenter" />
<AppBarButton Grid.Column="3"
Icon="AlignRight" />
<AppBarButton Grid.Row="1"
Grid.Column="1"
Icon="FontIncrease" />
<AppBarButton Grid.Row="1"
Grid.Column="2"
Icon="FontDecrease" />
<AppBarButton Grid.Row="1"
Grid.Column="3"
Icon="FontColor" />
</Grid>
</controls:RibbonCollapsibleGroup>

<controls:RibbonCollapsibleGroup CollapsedAccessKey="G"
Label="Advanced"
Priority="5"
RequestedWidths="400,200,300">
<controls:RibbonCollapsibleGroup.IconSource>
<SymbolIconSource Symbol="AllApps" />
</controls:RibbonCollapsibleGroup.IconSource>
<GridView Height="96"
MaxWidth="400"
HorizontalAlignment="Stretch"
VerticalAlignment="Center"
BorderBrush="{ThemeResource CardStrokeColorDefaultBrush}"
BorderThickness="1"
ScrollViewer.HorizontalScrollBarVisibility="Disabled"
ScrollViewer.HorizontalScrollMode="Disabled"
ScrollViewer.VerticalScrollBarVisibility="Auto">
<GridView.ItemTemplate>
<DataTemplate>
<TextBlock Width="24"
Height="24"
HorizontalTextAlignment="Center"
Text="{Binding}" />
</DataTemplate>
</GridView.ItemTemplate>
<GridView.Items>
<x:Double>1</x:Double>
<x:Double>2</x:Double>
<x:Double>3</x:Double>
<x:Double>4</x:Double>
<x:Double>5</x:Double>
<x:Double>6</x:Double>
<x:Double>7</x:Double>
<x:Double>8</x:Double>
<x:Double>9</x:Double>
<x:Double>10</x:Double>
<x:Double>11</x:Double>
<x:Double>12</x:Double>
<x:Double>13</x:Double>
<x:Double>14</x:Double>
</GridView.Items>
</GridView>
</controls:RibbonCollapsibleGroup>

<controls:RibbonCollapsibleGroup CollapsedAccessKey="E"
Label="Commands"
Priority="2"
Style="{StaticResource RibbonRightCollapsibleGroupStyle}">
<controls:RibbonCollapsibleGroup.IconSource>
<SymbolIconSource Symbol="Library" />
</controls:RibbonCollapsibleGroup.IconSource>

<Grid>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="Auto" />
<ColumnDefinition Width="Auto" />
<ColumnDefinition Width="Auto" />
<ColumnDefinition Width="Auto" />
<ColumnDefinition Width="Auto" />
</Grid.ColumnDefinitions>
<Grid.RowDefinitions>
<RowDefinition Height="*" />
<RowDefinition Height="*" />
</Grid.RowDefinitions>

<AppBarButton Icon="Accept" />
<AppBarButton Grid.Column="1"
Icon="Favorite" />
<AppBarButton Grid.Column="2"
Icon="Filter" />
<AppBarButton Grid.Column="3"
Icon="Find" />
<AppBarButton Grid.Column="4"
Icon="Flag" />

<AppBarButton Grid.Row="1"
Icon="Folder" />
<AppBarButton Grid.Row="1"
Grid.Column="1"
Icon="Font" />
<AppBarButton Grid.Row="1"
Grid.Column="2"
Icon="FontColor" />
<AppBarButton Grid.Row="1"
Grid.Column="3"
Icon="FontDecrease" />
<AppBarButton Grid.Row="1"
Grid.Column="4"
Icon="FontIncrease" />
</Grid>
</controls:RibbonCollapsibleGroup>

<controls:RibbonGroup Label="Options"
Style="{StaticResource RibbonRightGroupStyle}">
<AppBarButton Icon="Setting" />
</controls:RibbonGroup>
</controls:Ribbon>
</StackPanel>
</Page>
16 changes: 16 additions & 0 deletions components/Ribbon/samples/RibbonCustomSample.xaml.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.

using CommunityToolkit.WinUI.Controls;

namespace RibbonExperiment.Samples;

/// <summary>
/// An example of the <see cref="Ribbon"/> control.
/// </summary>
[ToolkitSample(id: nameof(RibbonCustomSample), "Ribbon control sample", description: $"A sample for showing how to create and use a {nameof(Ribbon)} custom control.")]
public sealed partial class RibbonCustomSample : Page
{
public RibbonCustomSample() => InitializeComponent();
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
<Project>
<Import Project="$([MSBuild]::GetPathOfFileAbove(Directory.Build.props))" Condition="Exists('$([MSBuild]::GetPathOfFileAbove(Directory.Build.props))')" />

<PropertyGroup>
<ToolkitComponentName>Ribbon</ToolkitComponentName>
<Description>This package contains Ribbon.</Description>

<!-- Rns suffix is required for namespaces shared across projects. See https://github.com/CommunityToolkit/Labs-Windows/issues/152 -->
<RootNamespace>CommunityToolkit.WinUI.Controls.RibbonRns</RootNamespace>
</PropertyGroup>

<!-- Sets this up as a toolkit component's source project -->
<Import Project="$(ToolingDirectory)\ToolkitComponent.SourceProject.props" />
<ItemGroup>
<UpToDateCheckInput Remove="RibbonStyle.xaml" />
</ItemGroup>
</Project>
31 changes: 31 additions & 0 deletions components/Ribbon/src/Dependencies.props
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
<!--
WinUI 2 under UWP uses TargetFramework uap10.0.*
WinUI 3 under WinAppSdk uses TargetFramework net6.0-windows10.*
However, under Uno-powered platforms, both WinUI 2 and 3 can share the same TargetFramework.
MSBuild doesn't play nicely with this out of the box, so we've made it easy for you.
For .NET Standard packages, you can use the Nuget Package Manager in Visual Studio.
For UWP / WinAppSDK / Uno packages, place the package references here.
-->
<Project>
<!-- WinUI 2 / UWP -->
<ItemGroup Condition="'$(IsUwp)' == 'true'">
<!-- <PackageReference Include="Microsoft.Toolkit.Uwp.UI.Controls.Primitives" Version="7.1.2"/> -->
</ItemGroup>

<!-- WinUI 2 / Uno -->
<ItemGroup Condition="'$(IsUno)' == 'true' AND '$(WinUIMajorVersion)' == '2'">
<!-- <PackageReference Include="Uno.Microsoft.Toolkit.Uwp.UI.Controls.Primitives" Version="7.1.11"/> -->
</ItemGroup>

<!-- WinUI 3 / WinAppSdk -->
<ItemGroup Condition="'$(IsWinAppSdk)' == 'true'">
<!-- <PackageReference Include="CommunityToolkit.WinUI.UI.Controls.Primitives" Version="7.1.2"/> -->
</ItemGroup>

<!-- WinUI 3 / Uno -->
<ItemGroup Condition="'$(IsUno)' == 'true' AND '$(WinUIMajorVersion)' == '3'">
<!-- <PackageReference Include="Uno.CommunityToolkit.WinUI.UI.Controls.Primitives" Version="7.1.100-dev.15.g12261e2626"/> -->
</ItemGroup>
</Project>
44 changes: 44 additions & 0 deletions components/Ribbon/src/DoubleList.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.

using Windows.Foundation.Metadata;

namespace CommunityToolkit.WinUI.Controls;

/// <summary>
/// A list of <see cref="double"/> values.
/// </summary>
[CreateFromString(MethodName = "CommunityToolkit.WinUI.Controls.DoubleList.CreateFromString")]
public class DoubleList : List<double>
{
/// <summary>
/// Initializes a new instance of <see cref="DoubleList"/> that is empty and has the default
/// initial capacity.
/// </summary>
public DoubleList()
{
}

/// <summary>
/// Initializes a new instance of <see cref="DoubleList"/> that contains elements copied from
/// the specified collection and has sufficient capacity to accommodate the number of elements
/// copied.
/// </summary>
/// <param name="values">The collection whose elements are copied to the new list.</param>
public DoubleList(IEnumerable<double> values)
: base(values)
{
}

/// <summary>
/// Create a <see cref="DoubleList"/> from the <paramref name="value"/> string.
/// </summary>
/// <param name="value">The list of doubles separated by ','.</param>
/// <returns>A <see cref="DoubleList"/> instance with the content of <paramref name="value"/>.</returns>
public static DoubleList CreateFromString(string value)
{
var list = value.Split(',', StringSplitOptions.RemoveEmptyEntries).Select(double.Parse);
return new DoubleList(list);
}
}
9 changes: 9 additions & 0 deletions components/Ribbon/src/MultiTarget.props
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
<Project>
<PropertyGroup>
<!--
MultiTarget is a custom property that indicates which target a project is designed to be built for / run on.
Used to create project references, generate solution files, enable/disable TargetFrameworks, and build nuget packages.
-->
<MultiTarget>uwp;wasdk;wpf;wasm;linuxgtk;macos;ios;android;</MultiTarget>
</PropertyGroup>
</Project>
201 changes: 201 additions & 0 deletions components/Ribbon/src/Ribbon.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,201 @@
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.

using System.Collections.Specialized;

namespace CommunityToolkit.WinUI.Controls;

/// <summary>
/// An Office like Ribbon control which displays groups of commands. If there is not enough space to display all the groups,
/// some of them can be collapsed based on a priority order.
/// </summary>
[ContentProperty(Name = nameof(Items))]
[TemplatePart(Name = PanelTemplatePart, Type = typeof(Panel))]
[TemplatePart(Name = ScrollViewerTemplatePart, Type = typeof(ScrollViewer))]
[TemplatePart(Name = ScrollDecrementButtonTempatePart, Type = typeof(ButtonBase))]
[TemplatePart(Name = ScrollIncrementButtonTempatePart, Type = typeof(ButtonBase))]
[TemplateVisualState(GroupName = ScrollButtonGroupNameTemplatePart, Name = NoButtonsStateTemplatePart)]
[TemplateVisualState(GroupName = ScrollButtonGroupNameTemplatePart, Name = DecrementButtonStateTemplatePart)]
[TemplateVisualState(GroupName = ScrollButtonGroupNameTemplatePart, Name = IncrementButtonStateTemplatePart)]
[TemplateVisualState(GroupName = ScrollButtonGroupNameTemplatePart, Name = BothButtonsStateTemplatePart)]
public sealed partial class Ribbon : Control
{
private const string PanelTemplatePart = "Panel";
private const string ScrollViewerTemplatePart = "ScrollViewer";
private const string ScrollDecrementButtonTempatePart = "ScrollDecrementButton";
private const string ScrollIncrementButtonTempatePart = "ScrollIncrementButton";
private const string ScrollButtonGroupNameTemplatePart = "ScrollButtonGroup";
private const string NoButtonsStateTemplatePart = "NoButtons";
private const string DecrementButtonStateTemplatePart = "DecrementButton";
private const string IncrementButtonStateTemplatePart = "IncrementButton";
private const string BothButtonsStateTemplatePart = "BothButtons";

private Panel? _panel;
private ScrollViewer? _scrollViewer;
private ButtonBase? _decrementButton;
private ButtonBase? _incrementButton;
private readonly ObservableCollection<UIElement> _items;

/// <summary>
/// The DP to store the <see cref="ScrollStep"/> property value.
/// </summary>
public static readonly DependencyProperty ScrollStepProperty = DependencyProperty.Register(
nameof(ScrollStep),
typeof(double),
typeof(Ribbon),
new PropertyMetadata(20.0));

/// <summary>
/// The amount to add or remove from the current scroll position.
/// </summary>
public double ScrollStep
{
get => (double)GetValue(ScrollStepProperty);
set => SetValue(ScrollStepProperty, value);
}

public Ribbon()
{
DefaultStyleKey = typeof(Ribbon);

_items = [];
_items.CollectionChanged += OnItemsCollectionChanged;
}

public IList<UIElement> Items => _items;

protected override void OnApplyTemplate()
{
_panel = GetTemplateChild(PanelTemplatePart) as Panel;
if (_panel is not null)
{
foreach (var item in _items)
{
_panel.Children.Add(item);
}

_panel.SizeChanged += OnSizeChanged;
}

_decrementButton = GetTemplateChild(ScrollDecrementButtonTempatePart) as ButtonBase;
if (_decrementButton is not null)
{
_decrementButton.Click += OnDecrementScrollViewer;
}

_incrementButton = GetTemplateChild(ScrollIncrementButtonTempatePart) as ButtonBase;
if (_incrementButton is not null)
{
_incrementButton.Click += OnIncrementScrollViewer;
}

_scrollViewer = GetTemplateChild(ScrollViewerTemplatePart) as ScrollViewer;
if (_scrollViewer is not null)
{
_scrollViewer.ViewChanged += OnViewChanged;
_scrollViewer.SizeChanged += OnSizeChanged;
UpdateScrollButtonsState();
}
}

private void OnItemsCollectionChanged(object? sender, NotifyCollectionChangedEventArgs e)
{
if (_panel is null)
{
return;
}

switch (e.Action)
{
case NotifyCollectionChangedAction.Add:
if (e.NewItems is not null)
{
for (var i = 0; i < e.NewItems.Count; i++)
{
var element = (UIElement?)e.NewItems[i] ?? throw new ArgumentException("Item must not be null");
_panel.Children.Insert(e.NewStartingIndex + i, element);
}
}
break;
case NotifyCollectionChangedAction.Remove:
if (e.OldItems is not null)
{
for (var i = 0; i < e.OldItems.Count; i++)
{
var element = (UIElement?)e.OldItems[i] ?? throw new ArgumentException("Item must not be null");
_panel.Children.Insert(e.OldStartingIndex, element);
}
}
break;
case NotifyCollectionChangedAction.Replace:
if (e.NewItems is not null)
{
for (var i = 0; i < e.NewItems.Count; i++)
{
var element = (UIElement?)e.NewItems[i] ?? throw new ArgumentException("Item must not be null");
_panel.Children[e.NewStartingIndex + i] = element;
}
}
break;
case NotifyCollectionChangedAction.Move:
_panel.Children.Move((uint)e.OldStartingIndex, (uint)e.NewStartingIndex);
break;
case NotifyCollectionChangedAction.Reset:
_panel.Children.Clear();
if (e.NewItems is not null)
{
foreach (var newItem in e.NewItems)
{
_panel.Children.Add((UIElement)newItem);
}
}
break;
default:
throw new ArgumentException("Invalid value for NotifyCollectionChangedAction");
}
}

private void OnViewChanged(object? sender, ScrollViewerViewChangedEventArgs e) => UpdateScrollButtonsState();

private void OnSizeChanged(object? sender, SizeChangedEventArgs e) => UpdateScrollButtonsState();

private void UpdateScrollButtonsState()
{
if (_scrollViewer is null)
{
return;
}

if (_scrollViewer.ExtentWidth <= _scrollViewer.ViewportWidth)
{
VisualStateManager.GoToState(this, NoButtonsStateTemplatePart, useTransitions: true);
return;
}

var showDecrement = _scrollViewer.HorizontalOffset >= 1;
var showIncrement = _scrollViewer.ExtentWidth - _scrollViewer.HorizontalOffset - _scrollViewer.ViewportWidth >= 1;
if (showDecrement && showIncrement)
{
VisualStateManager.GoToState(this, BothButtonsStateTemplatePart, useTransitions: true);
}
else if (showDecrement)
{
VisualStateManager.GoToState(this, DecrementButtonStateTemplatePart, useTransitions: true);
}
else if (showIncrement)
{
VisualStateManager.GoToState(this, IncrementButtonStateTemplatePart, useTransitions: true);
}
else
{
VisualStateManager.GoToState(this, NoButtonsStateTemplatePart, useTransitions: true);
}
}

private void OnDecrementScrollViewer(object sender, RoutedEventArgs e)
=> _scrollViewer?.ChangeView(_scrollViewer.HorizontalOffset - ScrollStep, verticalOffset: null, zoomFactor: null);

private void OnIncrementScrollViewer(object sender, RoutedEventArgs e)
=> _scrollViewer?.ChangeView(_scrollViewer.HorizontalOffset + ScrollStep, verticalOffset: null, zoomFactor: null);
}
289 changes: 289 additions & 0 deletions components/Ribbon/src/RibbonCollapsibleGroup.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,289 @@
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.

using Windows.System;

namespace CommunityToolkit.WinUI.Controls;

/// <summary>
/// A group which can be collapsed if its content does not fit in the <see cref="Ribbon"/>'s available space.
/// If the content does not fit, the group will display a single button. Clicking on this button will open
/// a flyout containing the group's content.
/// </summary>
[ContentProperty(Name = nameof(Content))]
[TemplatePart(Name = VisibleContentContainerTemplatePart, Type = typeof(ContentPresenter))]
[TemplatePart(Name = CollapsedButtonTemplatePart, Type = typeof(Button))]
[TemplatePart(Name = CollapsedFlyoutTemplatePart, Type = typeof(Flyout))]
[TemplatePart(Name = CollapsedContentPresenterTemplatePart, Type = typeof(ContentPresenter))]
public partial class RibbonCollapsibleGroup : RibbonGroup
{
private const string VisibleContentContainerTemplatePart = "VisibleContentContainer";
private const string CollapsedButtonTemplatePart = "CollapsedButton";
private const string CollapsedFlyoutTemplatePart = "CollapsedFlyout";
private const string CollapsedContentPresenterTemplatePart = "CollapsedContentPresenter";

/// <summary>
/// The DP to store the <see cref="IconSource"/> property value.
/// </summary>
public static readonly DependencyProperty IconSourceProperty = DependencyProperty.Register(
nameof(IconSource),
typeof(IconSource),
typeof(RibbonCollapsibleGroup),
new PropertyMetadata(null));

/// <summary>
/// The group icon. It will only be displayed when the group is in the collapsed state.
/// </summary>
public IconSource IconSource
{
get => (IconSource)GetValue(IconSourceProperty);
set => SetValue(IconSourceProperty, value);
}

/// <summary>
/// The DP to store the <see cref="State"/> property value.
/// </summary>
public static readonly DependencyProperty StateProperty = DependencyProperty.Register(
nameof(State),
typeof(Visibility),
typeof(RibbonCollapsibleGroup),
new PropertyMetadata(Visibility.Visible, OnStatePropertyChanged));

/// <summary>
/// The state of the group.
/// </summary>
public Visibility State
{
get => (Visibility)GetValue(StateProperty);
set => SetValue(StateProperty, value);
}

/// <summary>
/// The DP to store the <see cref="AutoCloseFlyout"/> property value.
/// </summary>
public static readonly DependencyProperty AutoCloseFlyoutProperty = DependencyProperty.Register(
nameof(AutoCloseFlyout),
typeof(bool),
typeof(RibbonCollapsibleGroup),
new PropertyMetadata(true));

/// <summary>
/// True to automatically close the overflow flyout if one interactive element is clicked.
/// Note that the logic to detect the click is very basic. It will request the flyout to close
/// for all the handled pointer released events. It assumes that if the pointer has been handled
/// something reacted to it. It works well for buttons or check boxes but does not work for text
/// or combo boxes.
/// </summary>
public bool AutoCloseFlyout
{
get => (bool)GetValue(AutoCloseFlyoutProperty);
set => SetValue(AutoCloseFlyoutProperty, value);
}

/// <summary>
/// The DP to store the <see cref="Priority"/> property value.
/// </summary>
public static readonly DependencyProperty PriorityProperty = DependencyProperty.Register(
nameof(Priority),
typeof(int),
typeof(RibbonCollapsibleGroup),
new PropertyMetadata(0));

/// <summary>
/// The priority of the group.
/// The group with the lower priority will be the first one to be collapsed.
/// </summary>
public int Priority
{
get => (int)GetValue(PriorityProperty);
set => SetValue(PriorityProperty, value);
}


/// <summary>
/// The DP to store the <see cref="CollapsedAccessKey"/> property value.
/// </summary>
public static readonly DependencyProperty CollapsedAccessKeyProperty = DependencyProperty.Register(
nameof(CollapsedAccessKey),
typeof(string),
typeof(RibbonCollapsibleGroup),
new PropertyMetadata(string.Empty));

/// <summary>
/// The access key to access the collapsed button and open the flyout.
/// </summary>
public string CollapsedAccessKey
{
get => (string)GetValue(CollapsedAccessKeyProperty);
set => SetValue(CollapsedAccessKeyProperty, value);
}


/// <summary>
/// The DP to store the <see cref="RequestedWidths"/> property value.
/// </summary>
public static readonly DependencyProperty RequestedWidthsProperty = DependencyProperty.Register(
nameof(RequestedWidths),
typeof(DoubleList),
typeof(RibbonCollapsibleGroup),
new PropertyMetadata(null, OnRequestedWidthChanged));

private static void OnRequestedWidthChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
{
if (e.NewValue != null)
{
((DoubleList)e.NewValue).Sort();
}
}

/// <summary>
/// The list of requested widths for the group.
/// If null or empty, the group will automatically use the size of its content.
/// If set, the group will use the smallest provided width fitting in the ribbon.
/// This is useful if the group contains a variable size control which can adjust
/// its width (like a GridView with several items).
/// </summary>
public DoubleList RequestedWidths
{
get => (DoubleList)GetValue(RequestedWidthsProperty);
set => SetValue(RequestedWidthsProperty, value);
}

private ContentControl? _visibleContentContainer;
private ContentControl? _collapsedContentContainer;
private Button? _collapsedButton;
private Flyout? _collapsedFlyout;

public RibbonCollapsibleGroup()
=> DefaultStyleKey = typeof(RibbonCollapsibleGroup);

protected override void OnApplyTemplate()
{
if (_collapsedFlyout is not null)
{
_collapsedFlyout.Opened -= OnFlyoutOpened;
}

if (_collapsedContentContainer is not null)
{
_collapsedContentContainer.RemoveHandler(PointerReleasedEvent, new PointerEventHandler(OnFlyoutPointerReleased));
_collapsedContentContainer.RemoveHandler(KeyUpEvent, new KeyEventHandler(OnFlyoutKeyUp));
}

_visibleContentContainer = Get<ContentControl>(VisibleContentContainerTemplatePart);
_collapsedContentContainer = Get<ContentControl>(CollapsedContentPresenterTemplatePart);
_collapsedButton = Get<Button>(CollapsedButtonTemplatePart);
_collapsedFlyout = Get<Flyout>(CollapsedFlyoutTemplatePart);

if (_collapsedFlyout is not null)
{
_collapsedFlyout.Opened += OnFlyoutOpened;
}

if (_collapsedContentContainer is not null)
{
_collapsedContentContainer.AddHandler(PointerReleasedEvent, new PointerEventHandler(OnFlyoutPointerReleased), handledEventsToo: true);
_collapsedContentContainer.AddHandler(KeyUpEvent, new KeyEventHandler(OnFlyoutKeyUp), handledEventsToo: true);
}

UpdateState();
}

private T? Get<T>(string templatePart) where T : class => GetTemplateChild(templatePart) as T;

private void OnFlyoutOpened(object? sender, object e)
=> _collapsedContentContainer?.Focus(FocusState.Programmatic);

private void OnFlyoutPointerReleased(object sender, PointerRoutedEventArgs e)
=> AutoCollapseFlyout(e.Handled, e.OriginalSource);

private void OnFlyoutKeyUp(object sender, KeyRoutedEventArgs e)
{
if (e.Key != VirtualKey.Enter && e.Key != VirtualKey.Space)
{
return;
}

AutoCollapseFlyout(e.Handled, e.OriginalSource);
}

private void AutoCollapseFlyout(bool eventHasBeenHandled, object originalSource)
{
// We only consider events which have been processed since it usually means
// that a control has processed the event and that the click is not in an
// empty/non-interactive area.
if (eventHasBeenHandled && AutoCloseFlyout && _collapsedFlyout?.IsOpen == true && !DoesRoutedEventOriginateFromAFlyoutHost(originalSource as UIElement))
{
_collapsedFlyout.Hide();
}
}

private bool DoesRoutedEventOriginateFromAFlyoutHost(UIElement? source)
{
if (_collapsedContentContainer is null)
{
return false;
}

while (source != null && source != _collapsedContentContainer)
{
if (source is MUXC.DropDownButton ||
source is DropDownButton ||
source is ComboBox ||
source is ComboBoxItem ||
(source is Button buttonSource && buttonSource.Flyout != null) ||
(source is FrameworkElement frameworkSource && FlyoutBase.GetAttachedFlyout(frameworkSource) != null))
{
return true;
}

source = VisualTreeHelper.GetParent(source) as UIElement;
}

return false;
}

private static void OnStatePropertyChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
{
var group = (RibbonCollapsibleGroup)d;
group.UpdateState();
}

private void UpdateState()
{
switch (State)
{
case Visibility.Visible:
_collapsedFlyout?.Hide();

if (_collapsedContentContainer is not null && _visibleContentContainer is not null)
{
_collapsedContentContainer.Content = null;
_visibleContentContainer.Content = Content;
}

if (_collapsedButton is not null && _visibleContentContainer is not null)
{
_collapsedButton.Visibility = Visibility.Collapsed;
_visibleContentContainer.Visibility = Visibility.Visible;
}
break;
case Visibility.Collapsed:
if (_collapsedContentContainer is not null && _visibleContentContainer is not null)
{
_visibleContentContainer.Content = null;
_collapsedContentContainer.Content = Content;
}

if (_collapsedButton is not null && _visibleContentContainer is not null)
{
_visibleContentContainer.Visibility = Visibility.Collapsed;
_collapsedButton.Visibility = Visibility.Visible;
}
break;
default:
throw new ArgumentException("Invalid state");
}
}
}
51 changes: 51 additions & 0 deletions components/Ribbon/src/RibbonGroup.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.

namespace CommunityToolkit.WinUI.Controls;

/// <summary>
/// A basic group displayed in a <see cref="Ribbon"/> control.
/// It adds a <see cref="Label"/> to the wrapped <see cref="Content"/>.
/// </summary>
[ContentProperty(Name = nameof(Content))]
public partial class RibbonGroup : Control
{
/// <summary>
/// The DP to store the <see cref="Content"/> property value.
/// </summary>
public static readonly DependencyProperty ContentProperty = DependencyProperty.Register(
nameof(Content),
typeof(UIElement),
typeof(RibbonGroup),
new PropertyMetadata(null));

/// <summary>
/// The content of the group.
/// </summary>
public UIElement Content
{
get => (UIElement)GetValue(ContentProperty);
set => SetValue(ContentProperty, value);
}

/// <summary>
/// The DP to store the <see cref="Label"/> property value.
/// </summary>
public static readonly DependencyProperty LabelProperty = DependencyProperty.Register(
nameof(Label),
typeof(string),
typeof(RibbonGroup),
new PropertyMetadata(""));

/// <summary>
/// The label of the group.
/// </summary>
public string Label
{
get => (string)GetValue(LabelProperty);
set => SetValue(LabelProperty, value);
}

public RibbonGroup() => DefaultStyleKey = typeof(RibbonGroup);
}
136 changes: 136 additions & 0 deletions components/Ribbon/src/RibbonPanel.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,136 @@
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.

namespace CommunityToolkit.WinUI.Controls;

/// <summary>
/// A panel which will set the <see cref="RibbonCollapsibleGroup"/> items in a collapsed state if there is not enough space to render them.
/// It is used by the <see cref="Ribbon"/> control.
/// </summary>
internal sealed partial class RibbonPanel : Panel
{
private static readonly Size GroupAvailableSize = new(double.PositiveInfinity, double.PositiveInfinity);

protected override Size MeasureOverride(Size availableSize)
{
// We try to limit the layout changes if the parent scrollviewer is sending values with small changes.
availableSize.Width = Math.Floor(availableSize.Width);

var childrenByPriority = Children.OrderBy(c => c is RibbonCollapsibleGroup collapsibleGroup ? collapsibleGroup.Priority : 0);
var desiredSize = new Size();
foreach (var child in childrenByPriority)
{
var collapsibleGroup = child as RibbonCollapsibleGroup;
var requestedWidths = collapsibleGroup?.RequestedWidths;
if (requestedWidths is null || collapsibleGroup?.State == Visibility.Collapsed)
{
child.Measure(GroupAvailableSize);
}
else
{
// Get the closest match to remainingWidth or use infinite size if we do not have any match.
var remainingWidth = availableSize.Width - desiredSize.Width;
//var requestedWidth = requestedWidths.LastOrDefault(w => w <= remainingWidth, defaultValue: double.PositiveInfinity);
var matchingWidths = requestedWidths.Where(w => w <= remainingWidth);
var requestedWidth = matchingWidths.Any() ? matchingWidths.Last() : double.PositiveInfinity;
var fixedSize = new Size(requestedWidth, availableSize.Height);
child.Measure(fixedSize);
}

desiredSize.Width += child.DesiredSize.Width;
desiredSize.Height = Math.Max(desiredSize.Height, child.DesiredSize.Height);
}

if (desiredSize.Width > availableSize.Width)
{
// We need to collapse some groups.
// If there is no priority order we assume that the last items are the one which should collapse first.
var groups = Children.OfType<RibbonCollapsibleGroup>().Reverse().Where(g => g.State == Visibility.Visible).OrderByDescending(g => g.Priority);
foreach (var group in groups)
{
group.State = Visibility.Collapsed;
var previousSize = group.DesiredSize;
group.Measure(GroupAvailableSize);
var newSize = group.DesiredSize;

if (newSize.Width < previousSize.Width)
{
desiredSize.Width -= previousSize.Width - newSize.Width;
desiredSize.Height = Math.Max(desiredSize.Height, newSize.Height);
}
else
{
// The collapsed size is bigger so keep using the visible state.
group.Visibility = Visibility.Visible;
group.Measure(GroupAvailableSize);
}

if (desiredSize.Width < availableSize.Width)
{
// No need to collapse more groups.
break;
}
}
}
else if (desiredSize.Width < availableSize.Width)
{
// We have more space than needed, we check if we can expand some groups
var groups = Children.OfType<RibbonCollapsibleGroup>().Where(g => g.State == Visibility.Collapsed).OrderBy(g => g.Priority);
foreach (var group in groups)
{
var previousSize = group.DesiredSize;
group.State = Visibility.Visible;

var requestedWidths = group.RequestedWidths;
if (requestedWidths is null)
{
group.Measure(GroupAvailableSize);
}
else
{
// Get the closest match to remainingWidth or use infinite size if we do not have any match.
var remainingWidth = availableSize.Width + previousSize.Width - desiredSize.Width;
//var requestedWidth = requestedWidths.LastOrDefault(w => w <= remainingWidth, defaultValue: double.PositiveInfinity);
var matchingWidths = requestedWidths.Where(w => w <= remainingWidth);
var requestedWidth = matchingWidths.Any() ? matchingWidths.Last() : double.PositiveInfinity;
var fixedSize = new Size(requestedWidth, availableSize.Height);
group.Measure(fixedSize);
}

var newSize = group.DesiredSize;
var widthIncrease = newSize.Width - previousSize.Width;
if (desiredSize.Width + widthIncrease > availableSize.Width)
{
// Too wide, we revert the change
group.State = Visibility.Collapsed;
group.Measure(GroupAvailableSize);
break;
}

desiredSize.Width += widthIncrease;
desiredSize.Height = Math.Max(desiredSize.Height, newSize.Height);
}
}

return desiredSize;
}

protected override Size ArrangeOverride(Size finalSize)
{
var position = new Rect
{
Height = finalSize.Height
};

foreach (var child in Children)
{
position.Width = child.DesiredSize.Width;
child.Arrange(position);

position.X += position.Width;
}

return new Size(position.X, position.Height);
}
}
406 changes: 406 additions & 0 deletions components/Ribbon/src/RibbonStyle.xaml

Large diffs are not rendered by default.

8 changes: 8 additions & 0 deletions components/Ribbon/src/Themes/Generic.xaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
<ResourceDictionary xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml">

<ResourceDictionary.MergedDictionaries>
<ResourceDictionary Source="ms-appx:///CommunityToolkit.WinUI.Controls.Ribbon/RibbonStyle.xaml" />
</ResourceDictionary.MergedDictionaries>

</ResourceDictionary>
23 changes: 23 additions & 0 deletions components/Ribbon/tests/Ribbon.Tests.projitems
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
<?xml version="1.0" encoding="utf-8"?>
<Project xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
<PropertyGroup>
<MSBuildAllProjects Condition="'$(MSBuildVersion)' == '' Or '$(MSBuildVersion)' &lt; '16.0'">$(MSBuildAllProjects);$(MSBuildThisFileFullPath)</MSBuildAllProjects>
<HasSharedItems>true</HasSharedItems>
<SharedGUID>C18B1738-4AFB-42D9-AC10-298B8F45B71C</SharedGUID>
</PropertyGroup>
<PropertyGroup Label="Configuration">
<Import_RootNamespace>RibbonExperiment.Tests</Import_RootNamespace>
</PropertyGroup>
<ItemGroup>
<Compile Include="$(MSBuildThisFileDirectory)RibbonTestClass.cs" />
<Compile Include="$(MSBuildThisFileDirectory)RibbonTestPage.xaml.cs">
<DependentUpon>RibbonTestPage.xaml</DependentUpon>
</Compile>
</ItemGroup>
<ItemGroup>
<Page Include="$(MSBuildThisFileDirectory)RibbonTestPage.xaml">
<SubType>Designer</SubType>
<Generator>MSBuild:Compile</Generator>
</Page>
</ItemGroup>
</Project>
13 changes: 13 additions & 0 deletions components/Ribbon/tests/Ribbon.Tests.shproj
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
<?xml version="1.0" encoding="utf-8"?>
<Project ToolsVersion="15.0" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
<PropertyGroup Label="Globals">
<ProjectGuid>C18B1738-4AFB-42D9-AC10-298B8F45B71C</ProjectGuid>
<MinimumVisualStudioVersion>14.0</MinimumVisualStudioVersion>
</PropertyGroup>
<Import Project="$(MSBuildExtensionsPath)\$(MSBuildToolsVersion)\Microsoft.Common.props" Condition="Exists('$(MSBuildExtensionsPath)\$(MSBuildToolsVersion)\Microsoft.Common.props')" />
<Import Project="$(MSBuildExtensionsPath32)\Microsoft\VisualStudio\v$(VisualStudioVersion)\CodeSharing\Microsoft.CodeSharing.Common.Default.props" />
<Import Project="$(MSBuildExtensionsPath32)\Microsoft\VisualStudio\v$(VisualStudioVersion)\CodeSharing\Microsoft.CodeSharing.Common.props" />
<PropertyGroup />
<Import Project="Ribbon.Tests.projitems" Label="Shared" />
<Import Project="$(MSBuildExtensionsPath32)\Microsoft\VisualStudio\v$(VisualStudioVersion)\CodeSharing\Microsoft.CodeSharing.CSharp.targets" />
</Project>
43 changes: 43 additions & 0 deletions components/Ribbon/tests/RibbonTestClass.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.

using CommunityToolkit.Tests;
using CommunityToolkit.Tooling.TestGen;
using CommunityToolkit.WinUI.Controls;

namespace RibbonExperiment.Tests;

[TestClass]
public partial class RibbonTestClass : VisualUITestBase
{
[TestMethod]
public void SimpleSynchronousExampleTest()
{
var assembly = typeof(Ribbon).Assembly;
var type = assembly.GetType(typeof(Ribbon).FullName ?? string.Empty);

Assert.IsNotNull(type, "Could not find Ribbon type.");
Assert.AreEqual(typeof(Ribbon), type, "Type of Ribbon does not match expected type.");
}

// The UIThreadTestMethod automatically dispatches to the UI for us to work with UI objects.
[UIThreadTestMethod]
public void SimpleUIAttributeExampleTest()
{
var component = new Ribbon();
Assert.IsNotNull(component);
}

// The UIThreadTestMethod can also easily grab a XAML Page for us by passing its type as a parameter.
// This lets us actually test a control as it would behave within an actual application.
// The page will already be loaded by the time your test is called.
[UIThreadTestMethod]
public void SimpleUIExamplePageTest(RibbonTestPage page)
{
// You can use the Toolkit Visual Tree helpers here to find the component by type or name:
var component = page.FindDescendant<Ribbon>();

Assert.IsNotNull(component);
}
}
14 changes: 14 additions & 0 deletions components/Ribbon/tests/RibbonTestPage.xaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
<!-- Licensed to the .NET Foundation under one or more agreements. The .NET Foundation licenses this file to you under the MIT license. See the LICENSE file in the project root for more information. -->
<Page x:Class="RibbonExperiment.Tests.RibbonTestPage"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:controls="using:CommunityToolkit.WinUI.Controls"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
Background="{ThemeResource ApplicationPageBackgroundThemeBrush}"
mc:Ignorable="d">

<Grid>
<controls:Ribbon x:Name="RibbonControl" />
</Grid>
</Page>
10 changes: 10 additions & 0 deletions components/Ribbon/tests/RibbonTestPage.xaml.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.

namespace RibbonExperiment.Tests;

public sealed partial class RibbonTestPage : Page
{
public RibbonTestPage() => InitializeComponent();
}

Unchanged files with check annotations Beta

{
base.OnApplyTemplate();
SelectedIndex = _internalSelectedIndex;
PreviewKeyDown -= TokenView_PreviewKeyDown;

Check warning on line 49 in components/TokenView/src/TokenView/TokenView.cs

GitHub Actions / wasm-linux

Windows.UI.Xaml.UIElement.PreviewKeyDown is not implemented in Uno (https://aka.platform.uno/notimplemented?m=Windows.UI.Xaml.UIElement.PreviewKeyDown) (https://aka.platform.uno/notimplemented)
SizeChanged += TokenView_SizeChanged;
if (_tokenViewScroller != null)
{
_tokenViewScroller.Loaded += ScrollViewer_Loaded;
}
PreviewKeyDown += TokenView_PreviewKeyDown;

Check warning on line 63 in components/TokenView/src/TokenView/TokenView.cs

GitHub Actions / wasm-linux

Windows.UI.Xaml.UIElement.PreviewKeyDown is not implemented in Uno (https://aka.platform.uno/notimplemented?m=Windows.UI.Xaml.UIElement.PreviewKeyDown) (https://aka.platform.uno/notimplemented)
OnIsWrappedChanged();
}
if (_tokenViewScroller != null)
{
_tokenViewScroller.ViewChanging += _tokenViewScroller_ViewChanging;

Check warning on line 82 in components/TokenView/src/TokenView/TokenView.Events.cs

GitHub Actions / wasm-linux

Windows.UI.Xaml.Controls.ScrollViewer.ViewChanging is not implemented in Uno (https://aka.platform.uno/notimplemented?m=Windows.UI.Xaml.Controls.ScrollViewer.ViewChanging) (https://aka.platform.uno/notimplemented)
_tokenViewScrollBackButton = _tokenViewScroller.FindDescendant(TokenViewScrollBackButtonName) as ButtonBase;
_tokenViewScrollForwardButton = _tokenViewScroller.FindDescendant(TokenViewScrollForwardButtonName) as ButtonBase;
}