Skip to content

Commit

Permalink
Merge pull request #1965 from Nexus-Mods/feat/root-row-styling
Browse files Browse the repository at this point in the history
Trees root and child row styling
  • Loading branch information
Al12rs authored Sep 3, 2024
2 parents 2e29673 + a4b741e commit ec50fcb
Show file tree
Hide file tree
Showing 2 changed files with 103 additions and 50 deletions.
63 changes: 52 additions & 11 deletions src/NexusMods.App.UI/Controls/TreeDataGrid/CustomElementFactory.cs
Original file line number Diff line number Diff line change
@@ -1,4 +1,6 @@
using System.Text;
using Avalonia.Controls;
using Avalonia.Controls.Models.TreeDataGrid;
using Avalonia.Controls.Primitives;

namespace NexusMods.App.UI.Controls;
Expand All @@ -9,31 +11,70 @@ namespace NexusMods.App.UI.Controls;
/// </summary>
public class CustomElementFactory : TreeDataGridElementFactory
{
// NOTE(erri120): Used in styles, don't change!
private const string RootRowClass = "RootRow";

protected override Control CreateElement(object? data)
{
var element = base.CreateElement(data);

if (data is ICustomCell customCell)
switch (data)
{
element.Classes.Add(customCell.Id);
if (customCell.IsRoot)
{
element.Classes.Add("RootRow");
}
// Add RootRowClass to root rows
case IIndentedRow { Indent: 0 }:
element.Classes.Add(RootRowClass);
break;
// Add Id to custom cells
case ICustomCell customCell:
element.Classes.Add(customCell.Id);
break;
}

return element;
}

protected override string GetDataRecycleKey(object? data)
{
// TODO(erri120): I think we need to implement this, otherwise cells for one column get put in another column
return base.GetDataRecycleKey(data);
// NOTE(Al12rs): Cell recycling breaks Id and RootRowClass styling, since these are not reapplied when the cell is reused.
// To fix this, we restrict the recycling to only reuse cells with matching Id and RootRowClass.
// This is done by appending the Id and RootRowClass to the base key.

switch (data)
{
case IIndentedRow:
var rowKey = $"{base.GetDataRecycleKey(data)}|{RootRowClass}";
return string.Intern(rowKey);
case ICustomCell customCell:
{
// NOTE(Al12rs): the keys generated here should match the ones in GetElementRecycleKey, ensure order and format is the same
var cellKey = $"{base.GetDataRecycleKey(data)}|{customCell.Id}";
return string.Intern(cellKey);
}
default:
return base.GetDataRecycleKey(data);
}
}

protected override string GetElementRecycleKey(Control element)
{
// TODO(erri120): I think we need to implement this, otherwise cells for one column get put in another column
return base.GetElementRecycleKey(element);
// NOTE(Al12rs): Cell recycling breaks Id and RootRowClass styling, since these are not reapplied when the cell is reused.
// To fix this, we restrict the recycling to only reuse cells with matching Id and RootRowClass.
// This is done by appending the Id and RootRowClass to the base key.

var sb = new StringBuilder(value: base.GetElementRecycleKey(element));

if (element is not TreeDataGridCell or TreeDataGridRow)
return string.Intern(sb.ToString());

// NOTE(Al12rs): Order here should match the insertion order in CreateElement, first the id, then the RootRowClass
foreach (var className in element.Classes)
{
if (className is null) continue;
if (className.StartsWith(':')) continue;
sb.Append($"|{className}");
}

var key = string.Intern(sb.ToString());
return key;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -34,19 +34,18 @@
</Style>



<!-- Modlist and Library TreeDataGrid Styles -->
<Style Selector="TreeDataGrid.MainListsStyling">
<Style.Resources>
<!-- TreeDataGrid Sort Icon replacements -->
<StreamGeometry x:Key="TreeDataGridSortIconDescendingPath">M7 14L12 9L17 14H7Z</StreamGeometry>
<StreamGeometry x:Key="TreeDataGridSortIconAscendingPath">M0 0L5 5L10 0H0Z</StreamGeometry>

<!-- TreeDataGrid Expander Icon replacements -->
<StreamGeometry x:Key="TreeDataGridItemCollapsedChevronPathData">M7.15833 13.825L10.975 10L7.15833 6.175L8.33333 5L13.3333 10L8.33333 15L7.15833 13.825Z</StreamGeometry>
<StreamGeometry x:Key="TreeDataGridItemExpandedChevronPathData">M6.175 7.15833L10 10.975L13.825 7.15833L15 8.33333L10 13.3333L5 8.33333L6.175 7.15833Z</StreamGeometry>
</Style.Resources>

<Setter Property="BorderThickness" Value="0" />

<!-- Header area styling -->
Expand All @@ -61,32 +60,32 @@
<Style Selector="^ TreeDataGridColumnHeader">
<Setter Property="Foreground" Value="{StaticResource NeutralSubduedBrush}" />
<Setter Property="BorderThickness" Value="0,0,0,0" />

<!-- Padding gets inherited by the ContentPresenter, which means that the sorting icon is ignoring this. -->
<!-- To properly pad the sorting icon, we need to override the template and rearrange the content. -->
<Setter Property="Padding" Value="8,0" />

<Style Selector="^ TextBlock">
<Setter Property="Theme" Value="{StaticResource TitleXSSemiTheme}" />
</Style>

<!-- Sort Icon replacements -->
<Style Selector="^[SortDirection=Ascending] /template/ Path#SortIcon">
<Setter Property="IsVisible" Value="True" />
<Setter Property="Data" Value="{DynamicResource TreeDataGridSortIconAscendingPath}" />
<Setter Property="Height" Value="12"/>
<Setter Property="Width" Value="12"/>
<Setter Property="Height" Value="12" />
<Setter Property="Width" Value="12" />
</Style>

<Style Selector="^[SortDirection=Descending] /template/ Path#SortIcon">
<Setter Property="IsVisible" Value="True" />
<Setter Property="Data" Value="{DynamicResource TreeDataGridSortIconDescendingPath}" />
<Setter Property="Height" Value="12"/>
<Setter Property="Width" Value="12"/>
<Setter Property="Height" Value="12" />
<Setter Property="Width" Value="12" />
</Style>

</Style>


<!-- First column header padding -->
<Style Selector="^ TreeDataGridColumnHeader:nth-child(1)">
Expand All @@ -104,33 +103,39 @@
<!-- Row Style -->
<Style Selector="^ TreeDataGridRow">
<Setter Property="Height" Value="52" />
<Setter Property="Background" Value="{StaticResource SurfaceLowBrush}" />




<!-- Generic column cell padding -->
<Style Selector="^ :is(TreeDataGridCell)">
<Setter Property="Padding" Value="8,2" />
</Style>

<!-- First column cell padding -->
<Style Selector="^ :is(TreeDataGridCell):nth-child(1)">
<Setter Property="Padding" Value="0,2,0,2" />
<Setter Property="Padding" Value="12,2,0,2" />
</Style>

<!-- Last column cell padding -->
<Style Selector="^ :is(TreeDataGridCell):nth-last-child(1)">
<Setter Property="Padding" Value="8,2,24,2" />

</Style>




<!-- Expander Column styling -->
<Style Selector="^ TreeDataGridExpanderCell">

<!-- Nested column cell padding -->
<!-- Override padding since this is a wrapper cell -->
<Setter Property="Padding" Value="0,0,0,0" />

<!-- Nested cell padding -->
<Style Selector="^ Border#CellBorder :is(TreeDataGridCell)">
<!-- This goes from after the expander to the end of the cell, but parent cell padding is also applied. -->
<Setter Property="Padding" Value="4,0,0,0" />

</Style>

<!-- Update the padding converter on the Border inside the TreeDataGridExpanderCell template -->
<!-- Rest is copied from the default style: -->
<!-- https://github.com/AvaloniaUI/Avalonia.Controls.TreeDataGrid/blob/38dce086b6c06503ab29bf824063f6fd113ea309/src/Avalonia.Controls.TreeDataGrid/Themes/Generic.axaml#L226-L245 -->
Expand All @@ -142,19 +147,19 @@
BorderThickness="{TemplateBinding BorderThickness}"
CornerRadius="{TemplateBinding CornerRadius}"
Padding="{TemplateBinding Indent, Converter={x:Static converters1:TreeDataGridIndentToPaddingConverter.Instance}}">
<!-- This is to adjust the left padding based on the indent level of the row. -->
<!-- This is to adjust the left padding based on the indent level of the row. -->

<DockPanel>
<!-- Indent line element -->
<Border DockPanel.Dock="Left"
<Border DockPanel.Dock="Left"
Width="20"
IsVisible="{TemplateBinding Indent, Converter={x:Static converters1:TreeDataGridIndentToBoolConverter.Instance}}">
<!-- This is to hide the indent line when the row is not indented. -->
<!-- This is to hide the indent line when the row is not indented. -->
<Border HorizontalAlignment="Center"
Width="2"
Background="{StaticResource SurfaceTranslucentMidBrush}"/>
Background="{StaticResource SurfaceTranslucentMidBrush}" />
</Border>

<DockPanel>
<Border DockPanel.Dock="Left"
Margin="4 0"
Expand All @@ -163,15 +168,15 @@
Theme="{StaticResource TreeDataGridExpandCollapseChevron}"
Focusable="False"
IsChecked="{TemplateBinding IsExpanded, Mode=TwoWay}"
IsVisible="{TemplateBinding ShowExpander}"/>
IsVisible="{TemplateBinding ShowExpander}" />
</Border>
<Decorator Name="PART_Content" />
</DockPanel>
</DockPanel>
</Border>
</ControlTemplate>
</Setter>

<!-- Expander ToggleButton styling -->
<Style Selector="^ ToggleButton#TreeDataGridExpandCollapseChevron">
<Setter Property="Width" Value="20" />
Expand All @@ -188,12 +193,12 @@
<Setter Property="Height" Value="12" />
<Setter Property="Margin" Value="0" />
</Style>

<!-- Allow the ChevronPath to be replaced by using DynamicResource -->
<Style Selector="^ Path#ChevronPath">
<Setter Property="Data" Value="{DynamicResource TreeDataGridItemCollapsedChevronPathData}" />
</Style>

<Style Selector="^:checked /template/ Path#ChevronPath">
<Setter Property="Data" Value="{DynamicResource TreeDataGridItemExpandedChevronPathData}" />
</Style>
Expand All @@ -206,27 +211,36 @@
<Setter Property="Theme" Value="{StaticResource BodyMDNormalTheme}" />
</Style>

<!-- Except for expander column -->
<Style Selector="^ TreeDataGridExpanderCell">
<Style Selector="^ TreeDataGridTextCell /template/ Border#CellBorder > TextBlock">
<Setter Property="Foreground" Value="{StaticResource NeutralModerateBrush}" />
</Style>
<!-- Except for first column -->
<Style Selector="^ :is(TreeDataGridCell):nth-child(1) Border#CellBorder > TextBlock">
<Setter Property="Foreground" Value="{StaticResource NeutralModerateBrush}" />
</Style>

<!-- Root Rows styling -->
<!-- "RootRow" is a class that was manually added to the TreeDataGridRow that have indent level 0. -->
<!-- See `NexusMods.App.UI.Controls.CustomElementFactory.cs` for more information. -->
<Style Selector="^.RootRow /template/ TreeDataGridCellsPresenter#PART_CellsPresenter">
<Setter Property="Background" Value="{StaticResource SurfaceLowBrush}" />
</Style>

<!-- Child rows styling -->
<Style Selector="^:not(.RootRow) /template/ TreeDataGridCellsPresenter#PART_CellsPresenter">
<Setter Property="Background" Value="{StaticResource BrandTranslucentDark300Brush}" />
</Style>

<!-- Row hover styling -->
<Style Selector="^:pointerover /template/ TreeDataGridCellsPresenter#PART_CellsPresenter">
<Setter Property="Background" Value="{StaticResource SurfaceMidBrush}" />
</Style>

<!-- Row selection styling -->
<Style Selector="^:selected /template/ TreeDataGridCellsPresenter#PART_CellsPresenter">
<Setter Property="Background" Value="{StaticResource SurfaceHighBrush}" />
</Style>

</Style>
</Style>


<!-- White caret styling -->
<Style Selector="TreeDataGrid.TreeWhiteCaret">
<Style Selector="^ TreeDataGridExpanderCell > Border > DockPanel > Border > ToggleButton">
Expand All @@ -241,6 +255,4 @@
</Style>




</Styles>

0 comments on commit ec50fcb

Please sign in to comment.