Skip to content

Commit 3197b4b

Browse files
committed
refactor: use custom BranchTree instead of TreeView to improve performance
1 parent 1c524cf commit 3197b4b

File tree

8 files changed

+651
-591
lines changed

8 files changed

+651
-591
lines changed

src/Converters/IntConverters.cs

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
1-
using Avalonia.Data.Converters;
1+
using Avalonia;
2+
using Avalonia.Data.Converters;
23

34
namespace SourceGit.Converters
45
{
@@ -24,5 +25,8 @@ public static class IntConverters
2425

2526
public static readonly FuncValueConverter<int, bool> IsSubjectLengthGood =
2627
new FuncValueConverter<int, bool>(v => v <= ViewModels.Preference.Instance.SubjectGuideLength);
28+
29+
public static readonly FuncValueConverter<int, Thickness> ToTreeMargin =
30+
new FuncValueConverter<int, Thickness>(v => new Thickness(v * 16, 0, 0, 0));
2731
}
2832
}

src/ViewModels/BranchTreeNode.cs

Lines changed: 37 additions & 101 deletions
Original file line numberDiff line numberDiff line change
@@ -1,122 +1,62 @@
1-
using System;
1+
using System;
22
using System.Collections.Generic;
33
using System.IO;
44

55
using Avalonia;
66
using Avalonia.Collections;
7+
using Avalonia.Media;
78

89
using CommunityToolkit.Mvvm.ComponentModel;
910

1011
namespace SourceGit.ViewModels
1112
{
12-
public enum BranchTreeNodeType
13-
{
14-
DetachedHead,
15-
Remote,
16-
Folder,
17-
Branch,
18-
}
19-
2013
public class BranchTreeNode : ObservableObject
2114
{
22-
public const double DEFAULT_CORNER = 4.0;
23-
24-
public string Name { get; set; }
25-
public BranchTreeNodeType Type { get; set; }
26-
public object Backend { get; set; }
27-
public bool IsFiltered { get; set; }
28-
public List<BranchTreeNode> Children { get; set; } = new List<BranchTreeNode>();
29-
30-
public bool IsUpstreamTrackStatusVisible
31-
{
32-
get => IsBranch && !string.IsNullOrEmpty((Backend as Models.Branch).UpstreamTrackStatus);
33-
}
34-
35-
public string UpstreamTrackStatus
36-
{
37-
get => Type == BranchTreeNodeType.Branch ? (Backend as Models.Branch).UpstreamTrackStatus : "";
38-
}
39-
40-
public bool IsRemote
15+
public string Name { get; private set; } = string.Empty;
16+
public object Backend { get; private set; } = null;
17+
public int Depth { get; set; } = 0;
18+
public bool IsFiltered { get; set; } = false;
19+
public List<BranchTreeNode> Children { get; private set; } = new List<BranchTreeNode>();
20+
21+
public bool IsExpanded
4122
{
42-
get => Type == BranchTreeNodeType.Remote;
23+
get => _isExpanded;
24+
set => SetProperty(ref _isExpanded, value);
4325
}
44-
45-
public bool IsFolder
26+
27+
public CornerRadius CornerRadius
4628
{
47-
get => Type == BranchTreeNodeType.Folder;
29+
get => _cornerRadius;
30+
set => SetProperty(ref _cornerRadius, value);
4831
}
49-
32+
5033
public bool IsBranch
5134
{
52-
get => Type == BranchTreeNodeType.Branch;
35+
get => Backend is Models.Branch;
5336
}
5437

55-
public bool IsDetachedHead
56-
{
57-
get => Type == BranchTreeNodeType.DetachedHead;
58-
}
59-
60-
public bool IsCurrent
38+
public bool IsUpstreamTrackStatusVisible
6139
{
62-
get => IsBranch && (Backend as Models.Branch).IsCurrent;
40+
get => Backend is Models.Branch { IsLocal: true } branch && !string.IsNullOrEmpty(branch.UpstreamTrackStatus);
6341
}
6442

65-
public bool IsSelected
43+
public string UpstreamTrackStatus
6644
{
67-
get => _isSelected;
68-
set => SetProperty(ref _isSelected, value);
45+
get => Backend is Models.Branch branch ? branch.UpstreamTrackStatus : "";
6946
}
7047

71-
public bool IsExpanded
48+
public FontWeight NameFontWeight
7249
{
73-
get => _isExpanded;
74-
set => SetProperty(ref _isExpanded, value);
50+
get => Backend is Models.Branch { IsCurrent: true } ? FontWeight.Bold : FontWeight.Regular;
7551
}
7652

7753
public string Tooltip
7854
{
79-
get
80-
{
81-
if (Backend is Models.Branch b)
82-
return b.FriendlyName;
83-
84-
return null;
85-
}
86-
}
87-
88-
public CornerRadius CornerRadius
89-
{
90-
get => _cornerRadius;
91-
set => SetProperty(ref _cornerRadius, value);
55+
get => Backend is Models.Branch b ? b.FriendlyName : null;
9256
}
93-
94-
public void UpdateCornerRadius(ref BranchTreeNode prev)
95-
{
96-
if (_isSelected && prev != null && prev.IsSelected)
97-
{
98-
var prevTop = prev.CornerRadius.TopLeft;
99-
prev.CornerRadius = new CornerRadius(prevTop, 0);
100-
CornerRadius = new CornerRadius(0, DEFAULT_CORNER);
101-
}
102-
else if (CornerRadius.TopLeft != DEFAULT_CORNER ||
103-
CornerRadius.BottomLeft != DEFAULT_CORNER)
104-
{
105-
CornerRadius = new CornerRadius(DEFAULT_CORNER);
106-
}
107-
108-
prev = this;
109-
110-
if (!IsBranch && IsExpanded)
111-
{
112-
foreach (var child in Children)
113-
child.UpdateCornerRadius(ref prev);
114-
}
115-
}
116-
117-
private bool _isSelected = false;
57+
11858
private bool _isExpanded = false;
119-
private CornerRadius _cornerRadius = new CornerRadius(DEFAULT_CORNER);
59+
private CornerRadius _cornerRadius = new CornerRadius(4);
12060

12161
public class Builder
12262
{
@@ -133,7 +73,6 @@ public void Run(List<Models.Branch> branches, List<Models.Remote> remotes, bool
13373
var node = new BranchTreeNode()
13474
{
13575
Name = remote.Name,
136-
Type = BranchTreeNodeType.Remote,
13776
Backend = remote,
13877
IsExpanded = bForceExpanded || _expanded.Contains(path),
13978
};
@@ -176,9 +115,13 @@ private void CollectExpandedNodes(List<BranchTreeNode> nodes, string prefix)
176115
{
177116
foreach (var node in nodes)
178117
{
118+
if (node.Backend is Models.Branch)
119+
continue;
120+
179121
var path = prefix + "/" + node.Name;
180-
if (node.Type != BranchTreeNodeType.Branch && node.IsExpanded)
122+
if (node.IsExpanded)
181123
_expanded.Add(path);
124+
182125
CollectExpandedNodes(node.Children, path);
183126
}
184127
}
@@ -191,7 +134,6 @@ private void MakeBranchNode(Models.Branch branch, List<BranchTreeNode> roots, Di
191134
roots.Add(new BranchTreeNode()
192135
{
193136
Name = branch.Name,
194-
Type = BranchTreeNodeType.Branch,
195137
Backend = branch,
196138
IsExpanded = false,
197139
IsFiltered = isFiltered,
@@ -215,7 +157,6 @@ private void MakeBranchNode(Models.Branch branch, List<BranchTreeNode> roots, Di
215157
lastFolder = new BranchTreeNode()
216158
{
217159
Name = name,
218-
Type = BranchTreeNodeType.Folder,
219160
IsExpanded = bForceExpanded || branch.IsCurrent || _expanded.Contains(folder),
220161
};
221162
roots.Add(lastFolder);
@@ -226,7 +167,6 @@ private void MakeBranchNode(Models.Branch branch, List<BranchTreeNode> roots, Di
226167
var cur = new BranchTreeNode()
227168
{
228169
Name = name,
229-
Type = BranchTreeNodeType.Folder,
230170
IsExpanded = bForceExpanded || branch.IsCurrent || _expanded.Contains(folder),
231171
};
232172
lastFolder.Children.Add(cur);
@@ -238,10 +178,9 @@ private void MakeBranchNode(Models.Branch branch, List<BranchTreeNode> roots, Di
238178
sepIdx = branch.Name.IndexOf('/', start);
239179
}
240180

241-
lastFolder.Children.Add(new BranchTreeNode()
181+
lastFolder?.Children.Add(new BranchTreeNode()
242182
{
243183
Name = Path.GetFileName(branch.Name),
244-
Type = branch.IsHead ? BranchTreeNodeType.DetachedHead : BranchTreeNodeType.Branch,
245184
Backend = branch,
246185
IsExpanded = false,
247186
IsFiltered = isFiltered,
@@ -252,16 +191,13 @@ private void SortNodes(List<BranchTreeNode> nodes)
252191
{
253192
nodes.Sort((l, r) =>
254193
{
255-
if (l.Type == BranchTreeNodeType.DetachedHead)
256-
{
194+
if (l.Backend is Models.Branch { IsHead: true })
257195
return -1;
258-
}
259-
if (l.Type == r.Type)
260-
{
261-
return l.Name.CompareTo(r.Name);
262-
}
263196

264-
return (int)l.Type - (int)r.Type;
197+
if (l.Backend is Models.Branch)
198+
return r.Backend is Models.Branch ? string.Compare(l.Name, r.Name, StringComparison.Ordinal) : 1;
199+
200+
return r.Backend is Models.Branch ? -1 : string.Compare(l.Name, r.Name, StringComparison.Ordinal);
265201
});
266202

267203
foreach (var node in nodes)

src/ViewModels/Repository.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1905,7 +1905,7 @@ private BranchTreeNode.Builder BuildBranchTree(List<Models.Branch> branches, Lis
19051905
visibles.Add(b);
19061906
}
19071907

1908-
builder.Run(visibles, remotes, visibles.Count <= 20);
1908+
builder.Run(visibles, remotes, true);
19091909
}
19101910

19111911
return builder;

src/Views/BranchTree.axaml

Lines changed: 110 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,110 @@
1+
<UserControl xmlns="https://github.com/avaloniaui"
2+
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
3+
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
4+
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
5+
xmlns:c="using:SourceGit.Converters"
6+
xmlns:v="using:SourceGit.Views"
7+
xmlns:vm="using:SourceGit.ViewModels"
8+
mc:Ignorable="d" d:DesignWidth="800" d:DesignHeight="450"
9+
x:Class="SourceGit.Views.BranchTree"
10+
x:Name="ThisControl">
11+
<DataGrid x:Name="BranchesPresenter"
12+
ItemsSource="{Binding #ThisControl.Rows}"
13+
Height="{Binding #ThisControl.Height}"
14+
Background="Transparent"
15+
RowHeight="24"
16+
CanUserReorderColumns="False"
17+
CanUserResizeColumns="False"
18+
CanUserSortColumns="False"
19+
HorizontalAlignment="Stretch"
20+
HorizontalScrollBarVisibility="Disabled"
21+
VerticalScrollBarVisibility="Auto"
22+
HeadersVisibility="None"
23+
SelectionChanged="OnNodesSelectionChanged"
24+
ContextRequested="OnTreeContextRequested">
25+
<DataGrid.Styles>
26+
<Style Selector="DataGridRow" x:DataType="vm:BranchTreeNode">
27+
<Setter Property="CornerRadius" Value="{Binding CornerRadius}" />
28+
</Style>
29+
30+
<Style Selector="DataGridRow /template/ Border#RowBorder">
31+
<Setter Property="ClipToBounds" Value="True" />
32+
</Style>
33+
34+
<Style Selector="Grid.repository_leftpanel DataGridRow:pointerover /template/ Rectangle#BackgroundRectangle">
35+
<Setter Property="Fill" Value="{DynamicResource Brush.AccentHovered}" />
36+
<Setter Property="Opacity" Value=".5"/>
37+
</Style>
38+
<Style Selector="Grid.repository_leftpanel DataGridRow:selected /template/ Rectangle#BackgroundRectangle">
39+
<Setter Property="Fill" Value="{DynamicResource Brush.AccentHovered}" />
40+
<Setter Property="Opacity" Value="1"/>
41+
</Style>
42+
<Style Selector="Grid.repository_leftpanel:focus-within DataGridRow:selected /template/ Rectangle#BackgroundRectangle">
43+
<Setter Property="Fill" Value="{DynamicResource Brush.Accent}" />
44+
<Setter Property="Opacity" Value=".65"/>
45+
</Style>
46+
<Style Selector="Grid.repository_leftpanel:focus-within DataGridRow:selected:pointerover /template/ Rectangle#BackgroundRectangle">
47+
<Setter Property="Fill" Value="{DynamicResource Brush.Accent}" />
48+
<Setter Property="Opacity" Value=".8"/>
49+
</Style>
50+
</DataGrid.Styles>
51+
52+
<DataGrid.Columns>
53+
<DataGridTemplateColumn Width="*">
54+
<DataGridTemplateColumn.CellTemplate>
55+
<DataTemplate x:DataType="vm:BranchTreeNode">
56+
<Grid Height="24"
57+
Margin="{Binding Depth, Converter={x:Static c:IntConverters.ToTreeMargin}}"
58+
ColumnDefinitions="16,20,*,Auto,Auto"
59+
Background="Transparent"
60+
DoubleTapped="OnDoubleTappedBranchNode"
61+
ToolTip.Tip="{Binding Tooltip}">
62+
63+
<!-- Tree Expander -->
64+
<ToggleButton Grid.Column="0"
65+
Classes="tree_expander"
66+
Focusable="False"
67+
HorizontalAlignment="Center"
68+
IsChecked="{Binding IsExpanded}"
69+
IsHitTestVisible="False"
70+
IsVisible="{Binding !IsBranch}"/>
71+
72+
<!-- Icon -->
73+
<v:BranchTreeNodeIcon Grid.Column="1"
74+
Node="{Binding}"
75+
IsExpanded="{Binding IsExpanded}"/>
76+
77+
<!-- Name -->
78+
<TextBlock Grid.Column="2"
79+
Text="{Binding Name}"
80+
Classes="monospace"
81+
FontWeight="{Binding NameFontWeight}"/>
82+
83+
<!-- Tracking status -->
84+
<Border Grid.Column="3"
85+
Margin="8,0"
86+
Height="18"
87+
CornerRadius="9"
88+
VerticalAlignment="Center"
89+
Background="{DynamicResource Brush.Badge}"
90+
IsVisible="{Binding IsUpstreamTrackStatusVisible}">
91+
<TextBlock Classes="monospace" FontSize="10" HorizontalAlignment="Center" Margin="9,0" Text="{Binding UpstreamTrackStatus}" Foreground="{DynamicResource Brush.BadgeFG}"/>
92+
</Border>
93+
94+
<!-- Filter Toggle Button -->
95+
<ToggleButton Grid.Column="4"
96+
Classes="filter"
97+
Margin="0,0,8,0"
98+
Background="Transparent"
99+
IsCheckedChanged="OnToggleFilter"
100+
IsVisible="{Binding IsBranch}"
101+
IsChecked="{Binding IsFiltered}"
102+
ToolTip.Tip="{DynamicResource Text.Filter}"/>
103+
</Grid>
104+
</DataTemplate>
105+
</DataGridTemplateColumn.CellTemplate>
106+
</DataGridTemplateColumn>
107+
</DataGrid.Columns>
108+
</DataGrid>
109+
</UserControl>
110+

0 commit comments

Comments
 (0)