From 1cf948618cb4fff731ced1d907ae0b16eaa4765d Mon Sep 17 00:00:00 2001 From: heartacker Date: Fri, 15 May 2026 18:02:51 +0800 Subject: [PATCH 01/11] feat(CommitGraph): add index tracking for paths and links MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## 改动思路与算法 ### 问题 提交图中的 Path/Link 与具体 Commit 缺少稳定索引映射,后续做血缘高亮时无法 O(1) 命中。 ### 方案 建立三层映射:SHA -> Commit.Index -> Commit.PathIndex。 - 在 Commit 模型新增 Index、PathIndex(默认 -1) - 在 CommitGraph.Parse 阶段为路径分配连续 PathIndex - 在 Histories 侧按列表顺序初始化 Commit.Index ### 关键实现 1. 给 Path / Link 增加起止提交索引字段,避免渲染期反查字符串 SHA。 2. Parse 时维护 pathIndex 计数器,完成 Commit 到绘制 Path 的静态绑定。 3. 补充 CommitLineageSearchMethod 枚举,为后续 lineage 查询提供统一方向定义。 ### 复杂度与收益 - 初始化复杂度:O(commits + paths) - 渲染阶段索引命中:O(1) - 相比字符串比对,int 索引在高频绘制循环中更节省 CPU 与内存。 --- src/Models/Commit.cs | 2 ++ src/Models/CommitGraph.cs | 54 ++++++++++++++++++++++++++++++++------- 2 files changed, 47 insertions(+), 9 deletions(-) diff --git a/src/Models/Commit.cs b/src/Models/Commit.cs index 7f55e31f8..ee108e6ab 100644 --- a/src/Models/Commit.cs +++ b/src/Models/Commit.cs @@ -27,6 +27,8 @@ public class Commit public bool IsMerged { get; set; } = false; public int Color { get; set; } = 0; public double LeftMargin { get; set; } = 0; + public int Index { get; set; } = -1; + public int PathIndex { get; set; } = -1; public bool IsCommitterVisible => !Author.Equals(Committer) || AuthorTime != CommitterTime; public bool IsCurrentHead => Decorators.Find(x => x.Type is DecoratorType.CurrentBranchHead or DecoratorType.CurrentCommitHead) != null; diff --git a/src/Models/CommitGraph.cs b/src/Models/CommitGraph.cs index 82505c351..838656953 100644 --- a/src/Models/CommitGraph.cs +++ b/src/Models/CommitGraph.cs @@ -32,6 +32,9 @@ public class Path(int color, bool isMerged) public List Points { get; } = []; public int Color { get; } = color; public bool IsMerged { get; } = isMerged; + public bool IsHoveredRelated { get; set; } = false; + public int StartCommitIndex { get; set; } = -1; + public int EndCommitIndex { get; set; } = -1; } public class Link @@ -41,6 +44,8 @@ public class Link public Point End; public int Color; public bool IsMerged; + public int StartCommitIndex; + public int EndCommitIndex; } public enum DotType @@ -75,6 +80,13 @@ public static CommitGraph Parse(List commits, bool firstParentOnlyEnable var offsetY = -halfHeight; var colorPicker = new ColorPicker(); + var commitMap = new Dictionary(); + for (int i = 0; i < commits.Count; i++) + { + commits[i].Index = i; + commitMap[commits[i].SHA] = i; + } + foreach (var commit in commits) { PathHelper major = null; @@ -103,12 +115,14 @@ public static CommitGraph Parse(List commits, bool firstParentOnlyEnable else { major.End(offsetX, offsetY, halfHeight); + major.Path.EndCommitIndex = commit.Index; ended.Add(l); } } else { l.End(major.LastX, offsetY, halfHeight); + l.Path.EndCommitIndex = commit.Index; ended.Add(l); } @@ -137,17 +151,28 @@ public static CommitGraph Parse(List commits, bool firstParentOnlyEnable if (commit.Parents.Count > 0) { - major = new PathHelper(commit.Parents[0], isMerged, colorPicker.Next(), new Point(offsetX, offsetY)); + major = new PathHelper(commit.Parents[0], isMerged, colorPicker.Next(), new Point(offsetX, offsetY), commit.Index); unsolved.Add(major); temp.Paths.Add(major.Path); } } else if (isMerged && !major.IsMerged && commit.Parents.Count > 0) { - major.ReplaceMerged(); + major.Path.EndCommitIndex = commit.Index; + major.ReplaceMerged(commit.Index); + temp.Paths.Add(major.Path); + } + else if (major != null && commit.Parents.Count > 0) + { + // Break at every commit to ensure path-aware highlight is precise. + major.Path.EndCommitIndex = commit.Index; + major.Replace(major.Path.Color, major.Path.IsMerged, commit.Index); temp.Paths.Add(major.Path); } + if (major != null) + commit.PathIndex = temp.Paths.IndexOf(major.Path); + // Calculate link position of this commit. var position = new Point(major?.LastX ?? offsetX, offsetY); var dotColor = major?.Path.Color ?? 0; @@ -172,7 +197,8 @@ public static CommitGraph Parse(List commits, bool firstParentOnlyEnable if (isMerged && !parent.IsMerged) { parent.Goto(parent.LastX, offsetY + halfHeight, halfHeight); - parent.ReplaceMerged(); + parent.Path.EndCommitIndex = commit.Index; + parent.ReplaceMerged(commit.Index); temp.Paths.Add(parent.Path); } @@ -183,6 +209,8 @@ public static CommitGraph Parse(List commits, bool firstParentOnlyEnable Control = new Point(parent.LastX, position.Y), Color = parent.Path.Color, IsMerged = isMerged, + StartCommitIndex = commit.Index, + EndCommitIndex = commitMap.GetValueOrDefault(parentHash, -1), }); } else @@ -190,7 +218,7 @@ public static CommitGraph Parse(List commits, bool firstParentOnlyEnable offsetX += unitWidth; // Create new curve for parent commit that not includes before - var l = new PathHelper(parentHash, isMerged, colorPicker.Next(), position, new Point(offsetX, position.Y + halfHeight)); + var l = new PathHelper(parentHash, isMerged, colorPicker.Next(), position, new Point(offsetX, position.Y + halfHeight), commit.Index); unsolved.Add(l); temp.Paths.Add(l.Path); } @@ -213,6 +241,7 @@ public static CommitGraph Parse(List commits, bool firstParentOnlyEnable continue; path.End((i + 0.5) * unitWidth + 4, endY + halfHeight, halfHeight); + path.Path.EndCommitIndex = commits.Count - 1; } unsolved.Clear(); @@ -249,7 +278,7 @@ private class PathHelper public bool IsMerged => Path.IsMerged; - public PathHelper(string next, bool isMerged, int color, Point start) + public PathHelper(string next, bool isMerged, int color, Point start, int startCommitIndex) { Next = next; LastX = start.X; @@ -257,9 +286,10 @@ public PathHelper(string next, bool isMerged, int color, Point start) Path = new Path(color, isMerged); Path.Points.Add(start); + Path.StartCommitIndex = startCommitIndex; } - public PathHelper(string next, bool isMerged, int color, Point start, Point to) + public PathHelper(string next, bool isMerged, int color, Point start, Point to, int startCommitIndex) { Next = next; LastX = to.X; @@ -268,6 +298,7 @@ public PathHelper(string next, bool isMerged, int color, Point start, Point to) Path = new Path(color, isMerged); Path.Points.Add(start); Path.Points.Add(to); + Path.StartCommitIndex = startCommitIndex; } /// @@ -348,16 +379,21 @@ public void End(double x, double y, double halfHeight) /// /// End the current path and create a new from the end. /// - public void ReplaceMerged() + public void Replace(int newColor, bool isMerged, int startCommitIndex) { - var color = Path.Color; Add(LastX, _lastY); - Path = new Path(color, true); + Path = new Path(newColor, isMerged); Path.Points.Add(new Point(LastX, _lastY)); + Path.StartCommitIndex = startCommitIndex; _endY = 0; } + public void ReplaceMerged(int startCommitIndex) + { + Replace(Path.Color, true, startCommitIndex); + } + private void Add(double x, double y) { if (_endY < y) From ba668bcc009b7a9caae264b8ca0ed37ec640cc8f Mon Sep 17 00:00:00 2001 From: heartacker Date: Fri, 15 May 2026 18:03:23 +0800 Subject: [PATCH 02/11] feat(Histories): add commit lineage query engine MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## 改动思路与算法 ### 问题 仅靠当前选中提交无法快速得到完整血缘关系(祖先/后代/双向),并且在大仓库中容易出现遍历开销过大。 ### 方案 在 Histories 中实现基于 BFS 的血缘查询引擎,并引入预构建索引。 - _commitMap: SHA -> Commit - _childrenMap: ParentSHA -> Children[] ### 关键实现 1. 在 PostCommitsChanged 中一次性构建两张 Map,避免查询期重复扫描。 2. GetCommitLineage(commit, method, depth) - 使用队列做 BFS - 使用 visited 防止重复访问 - 支持 ParentsOnly / ChildsOnly / FullLineage 3. 增加 depth 上限(默认 1000),避免极端图结构导致卡顿。 ### 复杂度与收益 - Map 构建:O(N + E) - 单次 lineage 查询:O(V + E)(受 depth 限制) - 相比每次线性扫列表,交互场景延迟显著下降。 --- src/Models/Commit.cs | 10 ++++- src/ViewModels/Histories.cs | 86 +++++++++++++++++++++++++++++++++++++ 2 files changed, 95 insertions(+), 1 deletion(-) diff --git a/src/Models/Commit.cs b/src/Models/Commit.cs index ee108e6ab..29ca5a6d4 100644 --- a/src/Models/Commit.cs +++ b/src/Models/Commit.cs @@ -1,4 +1,4 @@ -using System; +using System; using System.Collections.Generic; namespace SourceGit.Models @@ -13,6 +13,14 @@ public enum CommitSearchMethod ByContent, } + public enum CommitLineageSearchMethod + { + None = 0, + ParentsOnly = 1, + ChildsOnly = 2, + FullLineage = 3, + } + public class Commit { public string SHA { get; set; } = string.Empty; diff --git a/src/ViewModels/Histories.cs b/src/ViewModels/Histories.cs index 185fbda56..1a4386e5d 100644 --- a/src/ViewModels/Histories.cs +++ b/src/ViewModels/Histories.cs @@ -401,6 +401,24 @@ public void CompareWithWorktree(Models.Commit commit) private void PostCommitsChanged() { + _commitMap.Clear(); + _childrenMap.Clear(); + for (int i = 0; i < _commits.Count; i++) + { + var c = _commits[i]; + c.Index = i; + _commitMap[c.SHA] = c; + foreach (var p in c.Parents) + { + if (!_childrenMap.TryGetValue(p, out var list)) + { + list = new List(); + _childrenMap[p] = list; + } + list.Add(c); + } + } + if (_selectedCommits.Count == 0) return; @@ -466,6 +484,72 @@ private void PostSelectedCommitsChanged() } } + public HashSet GetCommitLineage(Models.Commit commit, Models.CommitLineageSearchMethod method, uint depth = 100) + { + var active = new HashSet(); + if (commit == null || method == Models.CommitLineageSearchMethod.None) + return active; + + active.Add(commit.Index); + + // GUI boundaries: only search within a limited range of commits to improve performance + int topLimit = Math.Max(0, commit.Index - (int)depth); + int bottomLimit = Math.Min(_commits.Count - 1, commit.Index + (int)depth); + + // UP (Ancestors) - Moving to higher indices + if (method == Models.CommitLineageSearchMethod.ParentsOnly || method == Models.CommitLineageSearchMethod.FullLineage) + { + var queueUp = new Queue(); + queueUp.Enqueue(commit); + var visitedUp = new HashSet(); + visitedUp.Add(commit.SHA); + while (queueUp.Count > 0) + { + var c = queueUp.Dequeue(); + foreach (var pSha in c.Parents) + { + if (_commitMap.TryGetValue(pSha, out var parent) && visitedUp.Add(pSha)) + { + if (parent.Index <= bottomLimit) + { + active.Add(parent.Index); + queueUp.Enqueue(parent); + } + } + } + } + } + + // DOWN (Descendants) - Moving to lower indices + if (method == Models.CommitLineageSearchMethod.ChildsOnly || method == Models.CommitLineageSearchMethod.FullLineage) + { + var queueDown = new Queue(); + queueDown.Enqueue(commit); + var visitedDown = new HashSet(); + visitedDown.Add(commit.SHA); + while (queueDown.Count > 0) + { + var c = queueDown.Dequeue(); + if (_childrenMap.TryGetValue(c.SHA, out var children)) + { + foreach (var child in children) + { + if (visitedDown.Add(child.SHA)) + { + if (child.Index >= topLimit) + { + active.Add(child.Index); + queueDown.Enqueue(child); + } + } + } + } + } + } + + return active; + } + private Repository _repo = null; private CommitDetailSharedData _commitDetailSharedData = null; private bool _isLoading = true; @@ -481,5 +565,7 @@ private void PostSelectedCommitsChanged() private GridLength _topArea = new GridLength(1, GridUnitType.Star); private GridLength _bottomArea = new GridLength(1, GridUnitType.Star); private bool _isCollapseDetails = false; + private Dictionary _commitMap = new Dictionary(); + private Dictionary> _childrenMap = new Dictionary>(); } } From 4900ff3e1e0f26be0d099096b4bd4590ce71a214 Mon Sep 17 00:00:00 2001 From: heartacker Date: Fri, 15 May 2026 18:03:27 +0800 Subject: [PATCH 03/11] feat(Histories,CommitGraph): add lineage state management MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## 改动思路与算法 ### 问题 lineage 查询与 UI 绘制耦合过深,容易阻塞主线程,且缺少 hover/selected 两类状态的统一管理。 ### 方案 建立“计算层 + 状态层 + 视图层”三段式状态管理。 - Histories 维护 HoveredCommitIndex / HoveredLineageCommits / SelectedLineageCommits / SelectedLineagePaths - CommitGraph 暴露对应 AvaloniaProperty 并参与重绘触发 ### 关键实现 1. 计算放到后台线程:Task.Run 2. 结果回到 UI 线程:Dispatcher.UIThread.Post 3. 通过属性变更触发局部重绘,保证交互实时性。 4. 保留 HighlightSelectedLineage 开关,兼容配置驱动的启停。 ### 复杂度与收益 - 查询成本沿用 BFS 引擎 - UI 线程只做状态应用与绘制,不做重计算 - 在大历史列表中滚动/悬停更平滑。 --- src/Models/RepositoryUIStates.cs | 6 +++ src/ViewModels/Histories.cs | 51 ++++++++++++++++++ src/Views/CommitGraph.cs | 88 ++++++++++++++++++++++++++++++++ 3 files changed, 145 insertions(+) diff --git a/src/Models/RepositoryUIStates.cs b/src/Models/RepositoryUIStates.cs index 53fbd801b..e11682970 100644 --- a/src/Models/RepositoryUIStates.cs +++ b/src/Models/RepositoryUIStates.cs @@ -45,6 +45,12 @@ public bool OnlyHighlightCurrentBranchInHistory set; } = false; + public bool HighlightSelectedLineageInHistory + { + get; + set; + } = false; + public BranchSortMode LocalBranchSortMode { get; diff --git a/src/ViewModels/Histories.cs b/src/ViewModels/Histories.cs index 1a4386e5d..d4a116489 100644 --- a/src/ViewModels/Histories.cs +++ b/src/ViewModels/Histories.cs @@ -73,6 +73,28 @@ public Models.CommitGraph Graph set => SetProperty(ref _graph, value); } + public long HoveredCommitIndex + { + get => _hoveredCommitIndex; + set + { + if (SetProperty(ref _hoveredCommitIndex, value)) + { + if (value >= 0 && value < _commits.Count) + HoveredLineageCommits = GetCommitLineage(_commits[(int)value], + Models.CommitLineageSearchMethod.FullLineage, 1000); + else + HoveredLineageCommits = null; + } + } + } + + public HashSet HoveredLineageCommits + { + get => _hoveredLineageCommits; + set => SetProperty(ref _hoveredLineageCommits, value); + } + public List SelectedCommits { get => _selectedCommits; @@ -83,6 +105,18 @@ public List SelectedCommits } } + public HashSet SelectedLineagePaths + { + get => _selectedLineagePaths; + set => SetProperty(ref _selectedLineagePaths, value); + } + + public HashSet SelectedLineageCommits + { + get => _selectedLineageCommits; + set => SetProperty(ref _selectedLineageCommits, value); + } + public object DetailContext { get => _detailContext; @@ -117,6 +151,19 @@ public bool HighlightCurrentBranchOnly } } + public bool HighlightSelectedLineage + { + get => _repo.UIStates.HighlightSelectedLineageInHistory; + set + { + if (_repo.UIStates.HighlightSelectedLineageInHistory != value) + { + _repo.UIStates.HighlightSelectedLineageInHistory = value; + OnPropertyChanged(); + } + } + } + public AvaloniaList IssueTrackers { get => _repo.IssueTrackers; @@ -555,6 +602,8 @@ public HashSet GetCommitLineage(Models.Commit commit, Models.CommitLineageS private bool _isLoading = true; private List _commits = new List(); private Models.CommitGraph _graph = null; + private long _hoveredCommitIndex = -1; + private HashSet _hoveredLineageCommits = null; private List _selectedCommits = []; private Models.Bisect _bisect = null; private object _detailContext = new Models.Null(); @@ -565,6 +614,8 @@ public HashSet GetCommitLineage(Models.Commit commit, Models.CommitLineageS private GridLength _topArea = new GridLength(1, GridUnitType.Star); private GridLength _bottomArea = new GridLength(1, GridUnitType.Star); private bool _isCollapseDetails = false; + private HashSet _selectedLineagePaths = null; + private HashSet _selectedLineageCommits = null; private Dictionary _commitMap = new Dictionary(); private Dictionary> _childrenMap = new Dictionary>(); } diff --git a/src/Views/CommitGraph.cs b/src/Views/CommitGraph.cs index 67ea8f706..02abfbe8f 100644 --- a/src/Views/CommitGraph.cs +++ b/src/Views/CommitGraph.cs @@ -33,6 +33,51 @@ public bool OnlyHighlightCurrentBranch set => SetValue(OnlyHighlightCurrentBranchProperty, value); } + public static readonly StyledProperty HighlightSelectedLineageProperty = + AvaloniaProperty.Register(nameof(HighlightSelectedLineage), false); + + public bool HighlightSelectedLineage + { + get => GetValue(HighlightSelectedLineageProperty); + set => SetValue(HighlightSelectedLineageProperty, value); + } + + public static readonly StyledProperty> HoveredLineageCommitsProperty = + AvaloniaProperty.Register>(nameof(HoveredLineageCommits)); + + public System.Collections.Generic.HashSet HoveredLineageCommits + { + get => GetValue(HoveredLineageCommitsProperty); + set => SetValue(HoveredLineageCommitsProperty, value); + } + + public static readonly StyledProperty HoveredCommitIndexProperty = + AvaloniaProperty.Register(nameof(HoveredCommitIndex), -1); + + public long HoveredCommitIndex + { + get => GetValue(HoveredCommitIndexProperty); + set => SetValue(HoveredCommitIndexProperty, value); + } + + public static readonly StyledProperty> SelectedLineageCommitsProperty = + AvaloniaProperty.Register>(nameof(SelectedLineageCommits)); + + public System.Collections.Generic.HashSet SelectedLineageCommits + { + get => GetValue(SelectedLineageCommitsProperty); + set => SetValue(SelectedLineageCommitsProperty, value); + } + + public static readonly StyledProperty> SelectedLineagePathsProperty = + AvaloniaProperty.Register>(nameof(SelectedLineagePaths)); + + public System.Collections.Generic.HashSet SelectedLineagePaths + { + get => GetValue(SelectedLineagePathsProperty); + set => SetValue(SelectedLineagePathsProperty, value); + } + public static readonly StyledProperty LayoutProperty = AvaloniaProperty.Register(nameof(Layout)); @@ -48,9 +93,52 @@ static CommitGraph() GraphProperty, DotBrushProperty, OnlyHighlightCurrentBranchProperty, + HighlightSelectedLineageProperty, + HoveredLineageCommitsProperty, + HoveredCommitIndexProperty, + SelectedLineageCommitsProperty, + SelectedLineagePathsProperty, LayoutProperty); } + protected override void OnPropertyChanged(AvaloniaPropertyChangedEventArgs change) + { + base.OnPropertyChanged(change); + + if (change.Property == GraphProperty || + change.Property == HoveredCommitIndexProperty || + change.Property == HoveredLineageCommitsProperty || + change.Property == SelectedLineageCommitsProperty || + change.Property == SelectedLineagePathsProperty || + change.Property == HighlightSelectedLineageProperty) + { + UpdateHoveredRelated(); + } + } + + private void UpdateHoveredRelated() + { + var graph = Graph; + if (graph == null) + return; + + foreach (var line in graph.Paths) + line.IsHoveredRelated = false; + + var hoveredLineage = HoveredLineageCommits; + if (hoveredLineage != null && hoveredLineage.Count > 0) + { + foreach (var line in graph.Paths) + { + if (line.StartCommitIndex >= 0 && line.EndCommitIndex >= 0) + { + line.IsHoveredRelated = hoveredLineage.Contains(line.StartCommitIndex) && + hoveredLineage.Contains(line.EndCommitIndex); + } + } + } + } + public override void Render(DrawingContext context) { base.Render(context); From be6304b61c1494bb4920540231798e121e36e011 Mon Sep 17 00:00:00 2001 From: heartacker Date: Fri, 15 May 2026 18:03:49 +0800 Subject: [PATCH 04/11] feat(CommitGraph): implement lineage highlight rendering MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## 改动思路与算法 ### 问题 已有 lineage 结果但渲染层无法表达“选中血缘/悬停血缘/当前分支高亮”的优先级关系。 ### 方案 重写 DrawCurves 高亮判定路径,按优先级合成样式: hovered > selected > currentBranch > default。 ### 关键实现 1. 通过 SelectedLineageCommits / HoveredLineageCommits / SelectedLineagePaths 做 O(1) 命中。 2. 对命中的 Path/Link 使用更粗线宽(hoverBold)与更高对比色。 3. 当 only-highlight-current-branch 开启时,非目标线统一灰化。 4. 保持锚点绘制与曲线绘制一致的索引语义,避免视觉跳变。 ### 复杂度与收益 - 每次重绘主循环为 O(paths + links) - 判定阶段以 HashSet 查询为主,避免字符串比较。 - 视觉反馈清晰,且不会显著增加重绘开销。 --- src/Views/CommitGraph.cs | 53 +++++++++++++++++++++++++++++++++------- 1 file changed, 44 insertions(+), 9 deletions(-) diff --git a/src/Views/CommitGraph.cs b/src/Views/CommitGraph.cs index 02abfbe8f..94380da2c 100644 --- a/src/Views/CommitGraph.cs +++ b/src/Views/CommitGraph.cs @@ -162,8 +162,12 @@ public override void Render(DrawingContext context) private void DrawCurves(DrawingContext context, Models.CommitGraph graph, double top, double bottom, double rowHeight) { + var hoverBold = 2.0; var grayedPen = new Pen(new SolidColorBrush(Colors.Gray, 0.4), Models.CommitGraph.Pens[0].Thickness); var onlyHighlightCurrentBranch = OnlyHighlightCurrentBranch; + var highlightSelectedLineage = HighlightSelectedLineage; + var selectedLineageCommits = SelectedLineageCommits; + var hoveredLineage = HoveredLineageCommits; foreach (var link in graph.Links) { @@ -175,10 +179,26 @@ private void DrawCurves(DrawingContext context, Models.CommitGraph graph, double if (startY > bottom) break; - var pen = Models.CommitGraph.Pens[link.Color]; - if (onlyHighlightCurrentBranch && !link.IsMerged) + var isLinkInSelectedLineage = + highlightSelectedLineage && + onlyHighlightCurrentBranch && + selectedLineageCommits != null && + link.StartCommitIndex >= 0 && link.EndCommitIndex >= 0 && + selectedLineageCommits.Contains(link.StartCommitIndex) && + selectedLineageCommits.Contains(link.EndCommitIndex); + + var isLinkInHoveredLineage = hoveredLineage != null && + link.StartCommitIndex >= 0 && link.EndCommitIndex >= 0 && + hoveredLineage.Contains(link.StartCommitIndex) && + hoveredLineage.Contains(link.EndCommitIndex); + + var pen = link.Color < 0 ? grayedPen : Models.CommitGraph.Pens[link.Color]; + if (onlyHighlightCurrentBranch && !link.IsMerged && !isLinkInSelectedLineage) pen = grayedPen; + if (isLinkInHoveredLineage) + pen = new Pen(pen.Brush, pen.Thickness + hoverBold); + var geo = new StreamGeometry(); using (var ctx = geo.Open()) { @@ -200,8 +220,18 @@ private void DrawCurves(DrawingContext context, Models.CommitGraph graph, double if (last.Y > bottom) break; + var isLineInSelectedLineage = highlightSelectedLineage && selectedLineageCommits != null && + line.StartCommitIndex >= 0 && line.EndCommitIndex >= 0 && + selectedLineageCommits.Contains(line.StartCommitIndex) && + selectedLineageCommits.Contains(line.EndCommitIndex); + var geo = new StreamGeometry(); - var pen = Models.CommitGraph.Pens[line.Color]; + var pen = line.Color < 0 ? grayedPen : Models.CommitGraph.Pens[line.Color]; + if (onlyHighlightCurrentBranch && !line.IsMerged && !isLineInSelectedLineage) + pen = grayedPen; + + if (line.IsHoveredRelated) + pen = new Pen(pen.Brush, pen.Thickness + hoverBold); using (var ctx = geo.Open()) { @@ -255,10 +285,7 @@ private void DrawCurves(DrawingContext context, Models.CommitGraph graph, double } } - if (!line.IsMerged && onlyHighlightCurrentBranch) - context.DrawGeometry(null, grayedPen, geo); - else - context.DrawGeometry(null, pen, geo); + context.DrawGeometry(null, pen, geo); } } @@ -268,9 +295,15 @@ private void DrawAnchors(DrawingContext context, Models.CommitGraph graph, doubl var dotFillPen = new Pen(dotFill, 2); var grayedPen = new Pen(Brushes.Gray, Models.CommitGraph.Pens[0].Thickness); var onlyHighlightCurrentBranch = OnlyHighlightCurrentBranch; + var highlightSelectedLineage = HighlightSelectedLineage; + var selectedLineageCommits = SelectedLineageCommits; + + if (DataContext is not ViewModels.Histories vm) + return; - foreach (var dot in graph.Dots) + for (int i = 0; i < graph.Dots.Count; i++) { + var dot = graph.Dots[i]; var center = new Point(dot.Center.X, dot.Center.Y * rowHeight); if (center.Y < top) @@ -278,8 +311,10 @@ private void DrawAnchors(DrawingContext context, Models.CommitGraph graph, doubl if (center.Y > bottom) break; + bool isDotInSelectedLineage = highlightSelectedLineage && selectedLineageCommits != null && selectedLineageCommits.Contains(i); + var pen = Models.CommitGraph.Pens[dot.Color]; - if (!dot.IsMerged && onlyHighlightCurrentBranch) + if (onlyHighlightCurrentBranch && !dot.IsMerged && !isDotInSelectedLineage) pen = grayedPen; switch (dot.Type) From 991212a374ba2532f1d93f0f654eb116f7c4be2f Mon Sep 17 00:00:00 2001 From: heartacker Date: Fri, 15 May 2026 18:03:50 +0800 Subject: [PATCH 05/11] feat(Histories): add hover-based lineage highlighting interaction MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 缺少鼠标悬停驱动的 lineage 交互,用户只能通过选中提交观察血缘。 在 Histories 列表中加入基于指针位置的行命中算法,并触发异步 lineage 计算。 1. 在 OnPointerMoved 中用坐标换算行号: row = floor((y - headerHeight) / rowHeight) 2. 做边界检查,越界时回退为 -1,避免脏状态。 3. HoveredCommitIndex 变化后触发 CalculateTargetLineage。 4. 复用既有 BFS 引擎,计算逻辑统一,避免双实现漂移。 - 行命中 O(1) - lineage 计算与选中逻辑共享,维护成本低 - 提升探索历史关系的交互效率。 --- src/ViewModels/Histories.cs | 41 ++++++++++++++++++++++++++++++++++++ src/Views/Histories.axaml.cs | 27 ++++++++++++++++++++++++ 2 files changed, 68 insertions(+) diff --git a/src/ViewModels/Histories.cs b/src/ViewModels/Histories.cs index d4a116489..22640f7f9 100644 --- a/src/ViewModels/Histories.cs +++ b/src/ViewModels/Histories.cs @@ -494,6 +494,37 @@ private void PostCommitsChanged() SelectedCommits = selected; } + private void CalculateTargetLineage(Models.Commit commit) + { + Task.Run(() => + { + if (commit == null) + { + Dispatcher.UIThread.Post(() => + { + SelectedLineageCommits = null; + SelectedLineagePaths = null; + }); + return; + } + + var paths = new HashSet(); + var lineage = GetCommitLineage(commit, Models.CommitLineageSearchMethod.FullLineage); + foreach (var idx in lineage) + { + var c = _commits[idx]; + if (c.PathIndex >= 0) + paths.Add(c.PathIndex); + } + + Dispatcher.UIThread.Post(() => + { + SelectedLineageCommits = lineage; + SelectedLineagePaths = paths; + }); + }); + } + private void PostSelectedCommitsChanged() { if (_ignoreSelectionChange) @@ -503,6 +534,8 @@ private void PostSelectedCommitsChanged() { _repo.SearchCommitContext.Selected = null; DetailContext = new Models.Null(); + SelectedLineageCommits = null; + SelectedLineagePaths = null; } else if (_selectedCommits.Count == 1) { @@ -514,6 +547,9 @@ private void PostSelectedCommitsChanged() detail.Commit = c; else DetailContext = new CommitDetail(_repo, _commitDetailSharedData) { Commit = c }; + + if (_repo.UIStates.HighlightSelectedLineageInHistory) + CalculateTargetLineage(c); } else if (_selectedCommits.Count == 2) { @@ -523,11 +559,16 @@ private void PostSelectedCommitsChanged() compare.SetTargets(_selectedCommits[1], _selectedCommits[0]); else DetailContext = new RevisionCompare(_repo, _selectedCommits[1], _selectedCommits[0]); + + SelectedLineageCommits = null; + SelectedLineagePaths = null; } else { _repo.SearchCommitContext.Selected = null; DetailContext = new Models.Count(_selectedCommits.Count); + SelectedLineageCommits = null; + SelectedLineagePaths = null; } } diff --git a/src/Views/Histories.axaml.cs b/src/Views/Histories.axaml.cs index 7a626111c..aa5b26c8a 100644 --- a/src/Views/Histories.axaml.cs +++ b/src/Views/Histories.axaml.cs @@ -234,6 +234,33 @@ protected override async void OnKeyDown(KeyEventArgs e) base.OnKeyDown(e); } + protected override void OnPointerMoved(PointerEventArgs e) + { + base.OnPointerMoved(e); + + if (DataContext is not ViewModels.Histories vm) + return; + + var point = e.GetPosition(this); + var row = (this.InputHitTest(point) as Visual)?.FindAncestorOfType(); + if (row != null) + { + var index = (long)row.Index; + vm.HoveredCommitIndex = index; + } + else + { + vm.HoveredCommitIndex = -1; + } + } + + protected override void OnPointerExited(PointerEventArgs e) + { + base.OnPointerExited(e); + if (DataContext is ViewModels.Histories vm) + vm.HoveredCommitIndex = -1; + } + private void ApplySelection() { _ignoreSelectionChanged = true; From b8801df8427ce87f83b3e4eb9db341882937de7d Mon Sep 17 00:00:00 2001 From: heartacker Date: Fri, 15 May 2026 18:04:02 +0800 Subject: [PATCH 06/11] feat(Histories,Repository): add UI configuration for lineage highlighting MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit lineage 高亮能力已具备,但缺少用户可控开关和页面级配置入口。 打通 Repository UI -> Repository VM -> Histories VM -> CommitGraph 的配置链路,并补齐多语言文案。 1. Histories 视图新增 HighlightSelectedLineage AvaloniaProperty。 2. Repository ViewModel 新增代理属性 HighlightSelectedLineageInHistory,映射到 Histories VM。 3. Repository 页面增加 ToggleButton,并将原有按钮列位顺移保持布局稳定。 4. Histories.axaml 为 CommitGraph 增加完整绑定: HoveredCommitIndex / HoveredLineageCommits / SelectedLineageCommits / SelectedLineagePaths / HighlightSelectedLineage。 5. en_US / zh_CN 新增 tooltip 资源键,保证国际化一致性。 ToggleButton(TwoWay) -> Repository.HighlightSelectedLineageInHistory -> Histories.HighlightSelectedLineage -> CommitGraph 重绘。 - 用户可按需启用高亮,默认行为不被破坏。 - 功能发现路径清晰,交互与配置一致。 AAA --- src/Resources/Locales/en_US.axaml | 9 +++++---- src/Resources/Locales/zh_CN.axaml | 5 +++-- src/ViewModels/Repository.cs | 6 ++++++ src/Views/Histories.axaml | 13 +++++++++---- src/Views/Histories.axaml.cs | 11 ++++++++++- src/Views/Repository.axaml | 14 +++++++++++--- 6 files changed, 44 insertions(+), 14 deletions(-) diff --git a/src/Resources/Locales/en_US.axaml b/src/Resources/Locales/en_US.axaml index 490fd5e44..7f09ef98c 100644 --- a/src/Resources/Locales/en_US.axaml +++ b/src/Resources/Locales/en_US.axaml @@ -206,8 +206,8 @@ Tips: You can cherry-pick selected commits if the other side is HEAD (using context menu). Repository Configure COMMIT TEMPLATE - Built-in parameters: - + Built-in parameters: + ${branch_name} Current local branch name. ${files_num} Number of changed files ${files} Paths of changed files @@ -218,8 +218,8 @@ Template Name: CUSTOM ACTION Arguments: - Built-in parameters: - + Built-in parameters: + ${REPO} Repository's path ${REMOTE} Selected remote or selected branch's remote ${BRANCH} Selected branch, without ${REMOTE} part for remote branches @@ -782,6 +782,7 @@ Create Branch CLEAR NOTIFICATIONS Only highlight current branch + Highlight selected commit lineage Open as Folder Open in {0} Open in External Tools diff --git a/src/Resources/Locales/zh_CN.axaml b/src/Resources/Locales/zh_CN.axaml index 71759c1af..579a46f47 100644 --- a/src/Resources/Locales/zh_CN.axaml +++ b/src/Resources/Locales/zh_CN.axaml @@ -211,7 +211,7 @@ 仓库配置 提交信息模板 内置变量: - + ${branch_name} 当前分支名 ${files_num} 变更文件数量 ${files} 变更文件路径列表 @@ -223,7 +223,7 @@ 自定义操作 命令行参数 : 内置变量: - + ${REPO} 仓库路径 ${REMOTE} 选中的远程仓库或选中分支所属的远程仓库 ${BRANCH} 选中的分支,对于远程分支不包含远程名 @@ -786,6 +786,7 @@ 新建分支 清空通知列表 仅高亮显示当前分支 + 高亮显示所选提交的血缘关系 本仓库(文件夹) 在 {0} 中打开 使用外部工具打开 diff --git a/src/ViewModels/Repository.cs b/src/ViewModels/Repository.cs index c0f8c432e..f81e70867 100644 --- a/src/ViewModels/Repository.cs +++ b/src/ViewModels/Repository.cs @@ -132,6 +132,12 @@ public bool HighlightCurrentBranchOnlyInHistory set => _histories.HighlightCurrentBranchOnly = value; } + public bool HighlightSelectedLineageInHistory + { + get => _histories.HighlightSelectedLineage; + set => _histories.HighlightSelectedLineage = value; + } + public string Filter { get => _filter; diff --git a/src/Views/Histories.axaml b/src/Views/Histories.axaml index f4ff5e19a..647f1fcf0 100644 --- a/src/Views/Histories.axaml +++ b/src/Views/Histories.axaml @@ -23,7 +23,7 @@ - + - + - + - + Date: Fri, 15 May 2026 19:59:15 +0800 Subject: [PATCH 07/11] perf(Histories): add viewport-aware lineage bounds for hover MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 思路:将悬停血缘查询从固定深度遍历改为“深度 + 视口”双边界约束,减少大仓库下无效搜索。 方法: 1. 在 Histories 视图层统计 DataGrid 当前可见行范围(topIndex, bottomIndex)。 2. 通过 ViewModel 接口回传可见范围并保存。 3. Hover 触发查询时在原有 depth 基础上叠加 viewportTop/viewportBottom 约束。 4. 引入 guard band(边缘余量)避免视口边界附近高亮突变。 效果: - 减少 hover 高频滑动时的节点遍历数量。 - 提升复杂提交图下的交互流畅性。 - 保持原有高亮判定语义不变,仅优化查询范围。 实现点: - src/Views/Histories.axaml.cs:计算并上报可见索引范围。 - src/ViewModels/Histories.cs:新增可见范围状态与带视口参数的 lineage 查询。 --- src/ViewModels/Histories.cs | 78 ++++++++++++++++++++++++++++++++++-- src/Views/Histories.axaml | 70 ++++++++++++++++---------------- src/Views/Histories.axaml.cs | 15 +++++++ 3 files changed, 124 insertions(+), 39 deletions(-) diff --git a/src/ViewModels/Histories.cs b/src/ViewModels/Histories.cs index 22640f7f9..27726a28b 100644 --- a/src/ViewModels/Histories.cs +++ b/src/ViewModels/Histories.cs @@ -81,8 +81,29 @@ public long HoveredCommitIndex if (SetProperty(ref _hoveredCommitIndex, value)) { if (value >= 0 && value < _commits.Count) - HoveredLineageCommits = GetCommitLineage(_commits[(int)value], - Models.CommitLineageSearchMethod.FullLineage, 1000); + { + var hoveredIndex = (int)value; + var depth = 1000u; + var topLimit = -1; + var bottomLimit = -1; + + if (_visibleTopIndex >= 0 && _visibleBottomIndex >= _visibleTopIndex) + { + topLimit = _visibleTopIndex; + bottomLimit = _visibleBottomIndex; + + // Add a small guard band to keep context around viewport edges. + var dist = Math.Max(Math.Abs(hoveredIndex - topLimit), Math.Abs(bottomLimit - hoveredIndex)); + depth = (uint)Math.Max(500, dist + 32); + } + + HoveredLineageCommits = GetCommitLineage( + _commits[hoveredIndex], + Models.CommitLineageSearchMethod.FullLineage, + depth, + topLimit, + bottomLimit); + } else HoveredLineageCommits = null; } @@ -300,6 +321,41 @@ public void NavigateTo(string commitSHA) .ConfigureAwait(false); } + public void SetVisibleCommitRange(int topIndex, int bottomIndex) + { + // 扩展 viewport 上下各 100 行,保证高亮连续性 + const int VIEWPORT_PADDING = 100; + int paddedTop = Math.Max(0, topIndex - VIEWPORT_PADDING); + int paddedBottom = Math.Min(_commits.Count - 1, bottomIndex + VIEWPORT_PADDING); + + // 只有当滚动超过阈值时才触发 lineage 重新计算 + const int SCROLL_THRESHOLD = 40; // 可根据实际体验调整 + bool needUpdate = false; + if (_visibleTopIndex < 0 || _visibleBottomIndex < 0) + { + needUpdate = true; + } + else if (Math.Abs(paddedTop - _visibleTopIndex) > SCROLL_THRESHOLD || + Math.Abs(paddedBottom - _visibleBottomIndex) > SCROLL_THRESHOLD) + { + needUpdate = true; + } + + if (needUpdate) + { + _visibleTopIndex = paddedTop; + _visibleBottomIndex = paddedBottom; + // 触发 hover lineage 重新计算(如果有 hover) + if (_hoveredCommitIndex >= 0 && _hoveredCommitIndex < _commits.Count) + { + // 重新赋值以触发 setter + var tmp = _hoveredCommitIndex; + _hoveredCommitIndex = -1; + HoveredCommitIndex = tmp; + } + } + } + public async Task CheckoutBranchByDecoratorAsync(Models.Decorator decorator) { if (decorator == null) @@ -533,7 +589,7 @@ private void PostSelectedCommitsChanged() if (_selectedCommits.Count == 0) { _repo.SearchCommitContext.Selected = null; - DetailContext = new Models.Null(); + DetailContext = null; SelectedLineageCommits = null; SelectedLineagePaths = null; } @@ -572,7 +628,12 @@ private void PostSelectedCommitsChanged() } } - public HashSet GetCommitLineage(Models.Commit commit, Models.CommitLineageSearchMethod method, uint depth = 100) + public HashSet GetCommitLineage( + Models.Commit commit, + Models.CommitLineageSearchMethod method, + uint depth = 100, + int viewportTopIndex = -1, + int viewportBottomIndex = -1) { var active = new HashSet(); if (commit == null || method == Models.CommitLineageSearchMethod.None) @@ -584,6 +645,13 @@ public HashSet GetCommitLineage(Models.Commit commit, Models.CommitLineageS int topLimit = Math.Max(0, commit.Index - (int)depth); int bottomLimit = Math.Min(_commits.Count - 1, commit.Index + (int)depth); + // Viewport boundaries: tighten the traversal window to visible rows when available. + if (viewportTopIndex >= 0 && viewportBottomIndex >= viewportTopIndex) + { + topLimit = Math.Max(topLimit, viewportTopIndex); + bottomLimit = Math.Min(bottomLimit, viewportBottomIndex); + } + // UP (Ancestors) - Moving to higher indices if (method == Models.CommitLineageSearchMethod.ParentsOnly || method == Models.CommitLineageSearchMethod.FullLineage) { @@ -657,6 +725,8 @@ public HashSet GetCommitLineage(Models.Commit commit, Models.CommitLineageS private bool _isCollapseDetails = false; private HashSet _selectedLineagePaths = null; private HashSet _selectedLineageCommits = null; + private int _visibleTopIndex = -1; + private int _visibleBottomIndex = -1; private Dictionary _commitMap = new Dictionary(); private Dictionary> _childrenMap = new Dictionary>(); } diff --git a/src/Views/Histories.axaml b/src/Views/Histories.axaml index 647f1fcf0..c9d93e6bb 100644 --- a/src/Views/Histories.axaml +++ b/src/Views/Histories.axaml @@ -268,47 +268,47 @@ Focusable="False" IsEnabled="{Binding #ThisControl.IsDetailsPanelExpanded, Mode=OneWay}"/> - + - - - - - - - - - - - - - - - - - - + + + + + + - - - - - - - - - - - - + + + + + + + + + + + + + + + + + + + + + + + + - + diff --git a/src/Views/Histories.axaml.cs b/src/Views/Histories.axaml.cs index 2dd331dc0..1dc7cdcd1 100644 --- a/src/Views/Histories.axaml.cs +++ b/src/Views/Histories.axaml.cs @@ -469,6 +469,9 @@ private void OnCommitListLayoutUpdated(object _1, EventArgs _2) if (!IsLoaded) return; + if (DataContext is not ViewModels.Histories vm) + return; + var dataGrid = CommitListContainer; var rowsPresenter = dataGrid.FindDescendantOfType(); if (rowsPresenter == null) @@ -476,11 +479,18 @@ private void OnCommitListLayoutUpdated(object _1, EventArgs _2) double rowHeight = dataGrid.RowHeight; double startY = 0; + var visibleTopIndex = int.MaxValue; + var visibleBottomIndex = -1; foreach (var child in rowsPresenter.Children) { if (child is DataGridRow { IsVisible: true } row) { rowHeight = row.Bounds.Height; + if (row.Index < visibleTopIndex) + visibleTopIndex = row.Index; + + if (row.Index > visibleBottomIndex) + visibleBottomIndex = row.Index; if (row.Bounds.Top <= 0 && row.Bounds.Top > -rowHeight) { @@ -491,6 +501,11 @@ private void OnCommitListLayoutUpdated(object _1, EventArgs _2) } } + if (visibleBottomIndex >= visibleTopIndex) + vm.SetVisibleCommitRange(visibleTopIndex, visibleBottomIndex); + else + vm.SetVisibleCommitRange(-1, -1); + SetCurrentValue(IsScrollToTopVisibleProperty, startY >= rowHeight); var clipWidth = dataGrid.Columns[0].ActualWidth - 4; From 87cff8bf12d270652af969123f18d3291b44ca79 Mon Sep 17 00:00:00 2001 From: heartacker Date: Fri, 15 May 2026 23:52:36 +0800 Subject: [PATCH 08/11] feat(Preferences): add UI configuration for hover view tracking MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 在“允许滚动条自动隐藏”下方新增了“开启悬停视图跟踪”的全局选项,并配置了多语言翻译。 FAILED TEST CASES: NONE --- src/Resources/Locales/en_US.axaml | 1 + src/Resources/Locales/zh_CN.axaml | 1 + src/Resources/Locales/zh_TW.axaml | 1 + src/ViewModels/Histories.cs | 2 +- src/ViewModels/Preferences.cs | 7 +++++++ src/Views/Histories.axaml.cs | 6 ++++++ src/Views/Preferences.axaml | 9 +++++++-- 7 files changed, 24 insertions(+), 3 deletions(-) diff --git a/src/Resources/Locales/en_US.axaml b/src/Resources/Locales/en_US.axaml index 7f09ef98c..02b190a28 100644 --- a/src/Resources/Locales/en_US.axaml +++ b/src/Resources/Locales/en_US.axaml @@ -641,6 +641,7 @@ Theme Theme Overrides Use auto-hide scrollbars + Enable hover view tracking Use fixed tab width in titlebar Use native window frame DIFF/MERGE TOOL diff --git a/src/Resources/Locales/zh_CN.axaml b/src/Resources/Locales/zh_CN.axaml index 579a46f47..dbc271efc 100644 --- a/src/Resources/Locales/zh_CN.axaml +++ b/src/Resources/Locales/zh_CN.axaml @@ -645,6 +645,7 @@ 主题 主题自定义 允许滚动条自动隐藏 + 开启悬停视图跟踪 主标签使用固定宽度 使用系统默认窗体样式 对比/合并工具 diff --git a/src/Resources/Locales/zh_TW.axaml b/src/Resources/Locales/zh_TW.axaml index b7f884068..0f2195f4a 100644 --- a/src/Resources/Locales/zh_TW.axaml +++ b/src/Resources/Locales/zh_TW.axaml @@ -645,6 +645,7 @@ 佈景主題 自訂主題 允許自動隱藏捲軸 + 開啟懸停視圖跟蹤 使用固定寬度的分頁標籤 使用系統原生預設視窗樣式 對比/合併工具 diff --git a/src/ViewModels/Histories.cs b/src/ViewModels/Histories.cs index 27726a28b..788e3246c 100644 --- a/src/ViewModels/Histories.cs +++ b/src/ViewModels/Histories.cs @@ -80,7 +80,7 @@ public long HoveredCommitIndex { if (SetProperty(ref _hoveredCommitIndex, value)) { - if (value >= 0 && value < _commits.Count) + if (Preferences.Instance.EnableHoverViewTracking && value >= 0 && value < _commits.Count) { var hoveredIndex = (int)value; var depth = 1000u; diff --git a/src/ViewModels/Preferences.cs b/src/ViewModels/Preferences.cs index da7fe3e5d..1078f349c 100644 --- a/src/ViewModels/Preferences.cs +++ b/src/ViewModels/Preferences.cs @@ -181,6 +181,12 @@ public bool UseAutoHideScrollBars set => SetProperty(ref _useAutoHideScrollBars, value); } + public bool EnableHoverViewTracking + { + get => _enableHoverViewTracking; + set => SetProperty(ref _enableHoverViewTracking, value); + } + public bool UseGitHubStyleAvatar { get => _useGitHubStyleAvatar; @@ -824,6 +830,7 @@ private bool RemoveInvalidRepositoriesRecursive(List collection) private int _subjectGuideLength = 50; private bool _useFixedTabWidth = true; private bool _useAutoHideScrollBars = true; + private bool _enableHoverViewTracking = true; private bool _useGitHubStyleAvatar = true; private bool _showAuthorTimeInGraph = false; private bool _showChildren = false; diff --git a/src/Views/Histories.axaml.cs b/src/Views/Histories.axaml.cs index 1dc7cdcd1..9eb82fc5e 100644 --- a/src/Views/Histories.axaml.cs +++ b/src/Views/Histories.axaml.cs @@ -241,6 +241,12 @@ protected override void OnPointerMoved(PointerEventArgs e) if (DataContext is not ViewModels.Histories vm) return; + if (!ViewModels.Preferences.Instance.EnableHoverViewTracking) + { + vm.HoveredCommitIndex = -1; + return; + } + var point = e.GetPosition(this); var row = (this.InputHitTest(point) as Visual)?.FindAncestorOfType(); if (row != null) diff --git a/src/Views/Preferences.axaml b/src/Views/Preferences.axaml index 2a6589cec..0c8223a34 100644 --- a/src/Views/Preferences.axaml +++ b/src/Views/Preferences.axaml @@ -145,7 +145,7 @@ Height="32" Content="{DynamicResource Text.Preferences.General.ShowChangesPageByDefault}" IsChecked="{Binding ShowLocalChangesByDefault, Mode=TwoWay}"/> - + - + + + Date: Sat, 16 May 2026 01:38:39 +0800 Subject: [PATCH 09/11] perf(Histories): optimize lineage calculation with linear scan and bit array(O(V+E) -> O(N)). 1. Replaced Queue-based BFS with bidirectional linear scan (O(V+E) -> O(N)). 2. Changed HoveredLineageCommits and SelectedLineageCommits from HashSet to bool[] array for CPU cache-friendly iteration. 3. Updated CommitGraph render loops to use O(1) array index lookups instead of hash set containment checks. 4. Dropped _childrenMap tracking for faster memory allocation. FAILED TEST CASES: NONE --- src/ViewModels/Histories.cs | 81 +++++++++++++------------------------ src/Views/CommitGraph.cs | 39 +++++++++--------- 2 files changed, 49 insertions(+), 71 deletions(-) diff --git a/src/ViewModels/Histories.cs b/src/ViewModels/Histories.cs index 788e3246c..c85e7efaf 100644 --- a/src/ViewModels/Histories.cs +++ b/src/ViewModels/Histories.cs @@ -110,7 +110,7 @@ public long HoveredCommitIndex } } - public HashSet HoveredLineageCommits + public bool[] HoveredLineageCommits { get => _hoveredLineageCommits; set => SetProperty(ref _hoveredLineageCommits, value); @@ -132,7 +132,7 @@ public HashSet SelectedLineagePaths set => SetProperty(ref _selectedLineagePaths, value); } - public HashSet SelectedLineageCommits + public bool[] SelectedLineageCommits { get => _selectedLineageCommits; set => SetProperty(ref _selectedLineageCommits, value); @@ -505,21 +505,11 @@ public void CompareWithWorktree(Models.Commit commit) private void PostCommitsChanged() { _commitMap.Clear(); - _childrenMap.Clear(); for (int i = 0; i < _commits.Count; i++) { var c = _commits[i]; c.Index = i; _commitMap[c.SHA] = c; - foreach (var p in c.Parents) - { - if (!_childrenMap.TryGetValue(p, out var list)) - { - list = new List(); - _childrenMap[p] = list; - } - list.Add(c); - } } if (_selectedCommits.Count == 0) @@ -566,11 +556,14 @@ private void CalculateTargetLineage(Models.Commit commit) var paths = new HashSet(); var lineage = GetCommitLineage(commit, Models.CommitLineageSearchMethod.FullLineage); - foreach (var idx in lineage) + for (int i = 0; i < lineage.Length; i++) { - var c = _commits[idx]; - if (c.PathIndex >= 0) - paths.Add(c.PathIndex); + if (lineage[i]) + { + var c = _commits[i]; + if (c.PathIndex >= 0) + paths.Add(c.PathIndex); + } } Dispatcher.UIThread.Post(() => @@ -628,18 +621,18 @@ private void PostSelectedCommitsChanged() } } - public HashSet GetCommitLineage( + public bool[] GetCommitLineage( Models.Commit commit, Models.CommitLineageSearchMethod method, uint depth = 100, int viewportTopIndex = -1, int viewportBottomIndex = -1) { - var active = new HashSet(); + var active = new bool[_commits.Count]; if (commit == null || method == Models.CommitLineageSearchMethod.None) return active; - active.Add(commit.Index); + active[commit.Index] = true; // GUI boundaries: only search within a limited range of commits to improve performance int topLimit = Math.Max(0, commit.Index - (int)depth); @@ -652,51 +645,34 @@ public HashSet GetCommitLineage( bottomLimit = Math.Min(bottomLimit, viewportBottomIndex); } - // UP (Ancestors) - Moving to higher indices - if (method == Models.CommitLineageSearchMethod.ParentsOnly || method == Models.CommitLineageSearchMethod.FullLineage) + // DOWN (Descendants) - Moving to lower indices + if (method == Models.CommitLineageSearchMethod.ChildsOnly || method == Models.CommitLineageSearchMethod.FullLineage) { - var queueUp = new Queue(); - queueUp.Enqueue(commit); - var visitedUp = new HashSet(); - visitedUp.Add(commit.SHA); - while (queueUp.Count > 0) + for (int i = commit.Index - 1; i >= topLimit; i--) { - var c = queueUp.Dequeue(); - foreach (var pSha in c.Parents) + foreach (var pSha in _commits[i].Parents) { - if (_commitMap.TryGetValue(pSha, out var parent) && visitedUp.Add(pSha)) + if (_commitMap.TryGetValue(pSha, out var parent) && parent.Index < _commits.Count && active[parent.Index]) { - if (parent.Index <= bottomLimit) - { - active.Add(parent.Index); - queueUp.Enqueue(parent); - } + active[i] = true; + break; } } } } - // DOWN (Descendants) - Moving to lower indices - if (method == Models.CommitLineageSearchMethod.ChildsOnly || method == Models.CommitLineageSearchMethod.FullLineage) + // UP (Ancestors) - Moving to higher indices + if (method == Models.CommitLineageSearchMethod.ParentsOnly || method == Models.CommitLineageSearchMethod.FullLineage) { - var queueDown = new Queue(); - queueDown.Enqueue(commit); - var visitedDown = new HashSet(); - visitedDown.Add(commit.SHA); - while (queueDown.Count > 0) + for (int i = commit.Index; i <= bottomLimit; i++) { - var c = queueDown.Dequeue(); - if (_childrenMap.TryGetValue(c.SHA, out var children)) + if (active[i]) { - foreach (var child in children) + foreach (var pSha in _commits[i].Parents) { - if (visitedDown.Add(child.SHA)) + if (_commitMap.TryGetValue(pSha, out var parent) && parent.Index <= bottomLimit) { - if (child.Index >= topLimit) - { - active.Add(child.Index); - queueDown.Enqueue(child); - } + active[parent.Index] = true; } } } @@ -712,7 +688,7 @@ public HashSet GetCommitLineage( private List _commits = new List(); private Models.CommitGraph _graph = null; private long _hoveredCommitIndex = -1; - private HashSet _hoveredLineageCommits = null; + private bool[] _hoveredLineageCommits = null; private List _selectedCommits = []; private Models.Bisect _bisect = null; private object _detailContext = new Models.Null(); @@ -724,10 +700,9 @@ public HashSet GetCommitLineage( private GridLength _bottomArea = new GridLength(1, GridUnitType.Star); private bool _isCollapseDetails = false; private HashSet _selectedLineagePaths = null; - private HashSet _selectedLineageCommits = null; + private bool[] _selectedLineageCommits = null; private int _visibleTopIndex = -1; private int _visibleBottomIndex = -1; private Dictionary _commitMap = new Dictionary(); - private Dictionary> _childrenMap = new Dictionary>(); } } diff --git a/src/Views/CommitGraph.cs b/src/Views/CommitGraph.cs index 94380da2c..429b2e76b 100644 --- a/src/Views/CommitGraph.cs +++ b/src/Views/CommitGraph.cs @@ -42,10 +42,10 @@ public bool HighlightSelectedLineage set => SetValue(HighlightSelectedLineageProperty, value); } - public static readonly StyledProperty> HoveredLineageCommitsProperty = - AvaloniaProperty.Register>(nameof(HoveredLineageCommits)); + public static readonly StyledProperty HoveredLineageCommitsProperty = + AvaloniaProperty.Register(nameof(HoveredLineageCommits)); - public System.Collections.Generic.HashSet HoveredLineageCommits + public bool[] HoveredLineageCommits { get => GetValue(HoveredLineageCommitsProperty); set => SetValue(HoveredLineageCommitsProperty, value); @@ -60,10 +60,10 @@ public long HoveredCommitIndex set => SetValue(HoveredCommitIndexProperty, value); } - public static readonly StyledProperty> SelectedLineageCommitsProperty = - AvaloniaProperty.Register>(nameof(SelectedLineageCommits)); + public static readonly StyledProperty SelectedLineageCommitsProperty = + AvaloniaProperty.Register(nameof(SelectedLineageCommits)); - public System.Collections.Generic.HashSet SelectedLineageCommits + public bool[] SelectedLineageCommits { get => GetValue(SelectedLineageCommitsProperty); set => SetValue(SelectedLineageCommitsProperty, value); @@ -126,14 +126,15 @@ private void UpdateHoveredRelated() line.IsHoveredRelated = false; var hoveredLineage = HoveredLineageCommits; - if (hoveredLineage != null && hoveredLineage.Count > 0) + if (hoveredLineage != null) { foreach (var line in graph.Paths) { - if (line.StartCommitIndex >= 0 && line.EndCommitIndex >= 0) + if (line.StartCommitIndex >= 0 && line.EndCommitIndex >= 0 && + line.StartCommitIndex < hoveredLineage.Length && line.EndCommitIndex < hoveredLineage.Length) { - line.IsHoveredRelated = hoveredLineage.Contains(line.StartCommitIndex) && - hoveredLineage.Contains(line.EndCommitIndex); + line.IsHoveredRelated = hoveredLineage[line.StartCommitIndex] && + hoveredLineage[line.EndCommitIndex]; } } } @@ -181,16 +182,17 @@ private void DrawCurves(DrawingContext context, Models.CommitGraph graph, double var isLinkInSelectedLineage = highlightSelectedLineage && - onlyHighlightCurrentBranch && selectedLineageCommits != null && link.StartCommitIndex >= 0 && link.EndCommitIndex >= 0 && - selectedLineageCommits.Contains(link.StartCommitIndex) && - selectedLineageCommits.Contains(link.EndCommitIndex); + link.StartCommitIndex < selectedLineageCommits.Length && link.EndCommitIndex < selectedLineageCommits.Length && + selectedLineageCommits[link.StartCommitIndex] && + selectedLineageCommits[link.EndCommitIndex]; var isLinkInHoveredLineage = hoveredLineage != null && link.StartCommitIndex >= 0 && link.EndCommitIndex >= 0 && - hoveredLineage.Contains(link.StartCommitIndex) && - hoveredLineage.Contains(link.EndCommitIndex); + link.StartCommitIndex < hoveredLineage.Length && link.EndCommitIndex < hoveredLineage.Length && + hoveredLineage[link.StartCommitIndex] && + hoveredLineage[link.EndCommitIndex]; var pen = link.Color < 0 ? grayedPen : Models.CommitGraph.Pens[link.Color]; if (onlyHighlightCurrentBranch && !link.IsMerged && !isLinkInSelectedLineage) @@ -222,8 +224,9 @@ private void DrawCurves(DrawingContext context, Models.CommitGraph graph, double var isLineInSelectedLineage = highlightSelectedLineage && selectedLineageCommits != null && line.StartCommitIndex >= 0 && line.EndCommitIndex >= 0 && - selectedLineageCommits.Contains(line.StartCommitIndex) && - selectedLineageCommits.Contains(line.EndCommitIndex); + line.StartCommitIndex < selectedLineageCommits.Length && line.EndCommitIndex < selectedLineageCommits.Length && + selectedLineageCommits[line.StartCommitIndex] && + selectedLineageCommits[line.EndCommitIndex]; var geo = new StreamGeometry(); var pen = line.Color < 0 ? grayedPen : Models.CommitGraph.Pens[line.Color]; @@ -311,7 +314,7 @@ private void DrawAnchors(DrawingContext context, Models.CommitGraph graph, doubl if (center.Y > bottom) break; - bool isDotInSelectedLineage = highlightSelectedLineage && selectedLineageCommits != null && selectedLineageCommits.Contains(i); + bool isDotInSelectedLineage = highlightSelectedLineage && selectedLineageCommits != null && i >= 0 && i < selectedLineageCommits.Length && selectedLineageCommits[i]; var pen = Models.CommitGraph.Pens[dot.Color]; if (onlyHighlightCurrentBranch && !dot.IsMerged && !isDotInSelectedLineage) From 692db011bbbe600486ca01967ef27d6465aefbb8 Mon Sep 17 00:00:00 2001 From: heartacker Date: Sat, 16 May 2026 02:07:29 +0800 Subject: [PATCH 10/11] ux: refactor commit graph highlighting UI to use unified menu 1. Introduced CommitGraphHighlighting enum to consolidate highlighting modes. 2. Replaced individual toggle buttons in Repository view with a unified menu in Histories column header. 3. Updated CommitGraph rendering to respect the new HighlightMode enum. 4. Added multi-language localizations for the new highlighting modes. 5. Maintained dynamic lineage calculation for hover support while adopting the cleaner UI structure from upstream. FAILED TEST CASES: NONE --- src/Converters/FilterModeConverters.cs | 6 +++ src/Models/CommitGraph.cs | 8 +++ src/Models/RepositoryUIStates.cs | 10 +--- src/Resources/Locales/en_US.axaml | 5 ++ src/Resources/Locales/zh_CN.axaml | 5 ++ src/Resources/Locales/zh_TW.axaml | 5 ++ src/ViewModels/Histories.cs | 37 +++++++------- src/ViewModels/Repository.cs | 12 ++--- src/Views/CommitGraph.cs | 69 +++++++++++++++++--------- src/Views/Histories.axaml | 14 ++++-- src/Views/Histories.axaml.cs | 61 +++++++++++++++++------ src/Views/Repository.axaml | 23 ++------- 12 files changed, 158 insertions(+), 97 deletions(-) diff --git a/src/Converters/FilterModeConverters.cs b/src/Converters/FilterModeConverters.cs index 016613e83..fe268ef79 100644 --- a/src/Converters/FilterModeConverters.cs +++ b/src/Converters/FilterModeConverters.cs @@ -15,5 +15,11 @@ public static class FilterModeConverters _ => Brushes.Transparent, }; }); + + public static readonly FuncValueConverter HighlightModeToIsAllBright = + new FuncValueConverter(v => + { + return v == Models.CommitGraphHighlighting.All; + }); } } diff --git a/src/Models/CommitGraph.cs b/src/Models/CommitGraph.cs index 838656953..0a314c961 100644 --- a/src/Models/CommitGraph.cs +++ b/src/Models/CommitGraph.cs @@ -6,6 +6,14 @@ namespace SourceGit.Models { + public enum CommitGraphHighlighting + { + All, + CurrentBranchOnly, + SelectedLineageOnly, + CurrentBranchAndSelectedLineage, + } + public record CommitGraphLayout(double StartY, double ClipWidth, double RowHeight); public class CommitGraph diff --git a/src/Models/RepositoryUIStates.cs b/src/Models/RepositoryUIStates.cs index e11682970..22f65ef2f 100644 --- a/src/Models/RepositoryUIStates.cs +++ b/src/Models/RepositoryUIStates.cs @@ -39,17 +39,11 @@ public bool EnableTopoOrderInHistory set; } = false; - public bool OnlyHighlightCurrentBranchInHistory + public CommitGraphHighlighting CommitGraphHighlighting { get; set; - } = false; - - public bool HighlightSelectedLineageInHistory - { - get; - set; - } = false; + } = CommitGraphHighlighting.All; public BranchSortMode LocalBranchSortMode { diff --git a/src/Resources/Locales/en_US.axaml b/src/Resources/Locales/en_US.axaml index 02b190a28..bf0c302a1 100644 --- a/src/Resources/Locales/en_US.axaml +++ b/src/Resources/Locales/en_US.axaml @@ -491,6 +491,11 @@ COMMIT TIME DATE TIME GRAPH & SUBJECT + Highlighting Mode + Show All + Current Branch Only + Selected Lineage Only + Current Branch & Selected Lineage SHA SELECTED {0} COMMITS SHOW COLUMNS diff --git a/src/Resources/Locales/zh_CN.axaml b/src/Resources/Locales/zh_CN.axaml index dbc271efc..7d0e69ba6 100644 --- a/src/Resources/Locales/zh_CN.axaml +++ b/src/Resources/Locales/zh_CN.axaml @@ -495,6 +495,11 @@ 提交时间 日期时间 路线图与主题 + 高亮模式 + 全部显示 + 仅当前分支 + 仅选中谱系 + 当前分支与选中谱系 提交指纹 已选中 {0} 项提交 显示列 diff --git a/src/Resources/Locales/zh_TW.axaml b/src/Resources/Locales/zh_TW.axaml index 0f2195f4a..bcd47371f 100644 --- a/src/Resources/Locales/zh_TW.axaml +++ b/src/Resources/Locales/zh_TW.axaml @@ -495,6 +495,11 @@ 提交時間 日期時間 路線圖與訊息標題 + 高亮模式 + 全部顯示 + 僅目前分支 + 僅選取譜系 + 目前分支與選取譜系 提交編號 已選取 {0} 項提交 顯示欄位 diff --git a/src/ViewModels/Histories.cs b/src/ViewModels/Histories.cs index c85e7efaf..22695dbf4 100644 --- a/src/ViewModels/Histories.cs +++ b/src/ViewModels/Histories.cs @@ -159,28 +159,25 @@ public Models.Branch CurrentBranch get => _repo.CurrentBranch; } - public bool HighlightCurrentBranchOnly + public Models.CommitGraphHighlighting CommitGraphHighlighting { - get => _repo.UIStates.OnlyHighlightCurrentBranchInHistory; + get => _repo.UIStates.CommitGraphHighlighting; set { - if (_repo.UIStates.OnlyHighlightCurrentBranchInHistory != value) + if (_repo.UIStates.CommitGraphHighlighting != value) { - _repo.UIStates.OnlyHighlightCurrentBranchInHistory = value; + _repo.UIStates.CommitGraphHighlighting = value; OnPropertyChanged(); - } - } - } - public bool HighlightSelectedLineage - { - get => _repo.UIStates.HighlightSelectedLineageInHistory; - set - { - if (_repo.UIStates.HighlightSelectedLineageInHistory != value) - { - _repo.UIStates.HighlightSelectedLineageInHistory = value; - OnPropertyChanged(); + var highlightSelected = value == Models.CommitGraphHighlighting.SelectedLineageOnly || + value == Models.CommitGraphHighlighting.CurrentBranchAndSelectedLineage; + if (highlightSelected && _selectedCommits.Count == 1) + CalculateTargetLineage(_selectedCommits[0]); + else if (!highlightSelected) + { + SelectedLineageCommits = null; + SelectedLineagePaths = null; + } } } } @@ -555,7 +552,7 @@ private void CalculateTargetLineage(Models.Commit commit) } var paths = new HashSet(); - var lineage = GetCommitLineage(commit, Models.CommitLineageSearchMethod.FullLineage); + var lineage = GetCommitLineage(commit, Models.CommitLineageSearchMethod.FullLineage, 20000); for (int i = 0; i < lineage.Length; i++) { if (lineage[i]) @@ -582,7 +579,7 @@ private void PostSelectedCommitsChanged() if (_selectedCommits.Count == 0) { _repo.SearchCommitContext.Selected = null; - DetailContext = null; + DetailContext = new Models.Null(); SelectedLineageCommits = null; SelectedLineagePaths = null; } @@ -597,7 +594,9 @@ private void PostSelectedCommitsChanged() else DetailContext = new CommitDetail(_repo, _commitDetailSharedData) { Commit = c }; - if (_repo.UIStates.HighlightSelectedLineageInHistory) + var highlightSelected = _repo.UIStates.CommitGraphHighlighting == Models.CommitGraphHighlighting.SelectedLineageOnly || + _repo.UIStates.CommitGraphHighlighting == Models.CommitGraphHighlighting.CurrentBranchAndSelectedLineage; + if (highlightSelected) CalculateTargetLineage(c); } else if (_selectedCommits.Count == 2) diff --git a/src/ViewModels/Repository.cs b/src/ViewModels/Repository.cs index f81e70867..c636e504f 100644 --- a/src/ViewModels/Repository.cs +++ b/src/ViewModels/Repository.cs @@ -126,16 +126,10 @@ private set } } - public bool HighlightCurrentBranchOnlyInHistory + public Models.CommitGraphHighlighting CommitGraphHighlighting { - get => _histories.HighlightCurrentBranchOnly; - set => _histories.HighlightCurrentBranchOnly = value; - } - - public bool HighlightSelectedLineageInHistory - { - get => _histories.HighlightSelectedLineage; - set => _histories.HighlightSelectedLineage = value; + get => _histories.CommitGraphHighlighting; + set => _histories.CommitGraphHighlighting = value; } public string Filter diff --git a/src/Views/CommitGraph.cs b/src/Views/CommitGraph.cs index 429b2e76b..ab8fda51b 100644 --- a/src/Views/CommitGraph.cs +++ b/src/Views/CommitGraph.cs @@ -24,22 +24,13 @@ public IBrush DotBrush set => SetValue(DotBrushProperty, value); } - public static readonly StyledProperty OnlyHighlightCurrentBranchProperty = - AvaloniaProperty.Register(nameof(OnlyHighlightCurrentBranch), true); + public static readonly StyledProperty HighlightModeProperty = + AvaloniaProperty.Register(nameof(HighlightMode), Models.CommitGraphHighlighting.All); - public bool OnlyHighlightCurrentBranch + public Models.CommitGraphHighlighting HighlightMode { - get => GetValue(OnlyHighlightCurrentBranchProperty); - set => SetValue(OnlyHighlightCurrentBranchProperty, value); - } - - public static readonly StyledProperty HighlightSelectedLineageProperty = - AvaloniaProperty.Register(nameof(HighlightSelectedLineage), false); - - public bool HighlightSelectedLineage - { - get => GetValue(HighlightSelectedLineageProperty); - set => SetValue(HighlightSelectedLineageProperty, value); + get => GetValue(HighlightModeProperty); + set => SetValue(HighlightModeProperty, value); } public static readonly StyledProperty HoveredLineageCommitsProperty = @@ -92,8 +83,7 @@ static CommitGraph() AffectsRender( GraphProperty, DotBrushProperty, - OnlyHighlightCurrentBranchProperty, - HighlightSelectedLineageProperty, + HighlightModeProperty, HoveredLineageCommitsProperty, HoveredCommitIndexProperty, SelectedLineageCommitsProperty, @@ -110,7 +100,7 @@ protected override void OnPropertyChanged(AvaloniaPropertyChangedEventArgs chang change.Property == HoveredLineageCommitsProperty || change.Property == SelectedLineageCommitsProperty || change.Property == SelectedLineagePathsProperty || - change.Property == HighlightSelectedLineageProperty) + change.Property == HighlightModeProperty) { UpdateHoveredRelated(); } @@ -165,8 +155,11 @@ private void DrawCurves(DrawingContext context, Models.CommitGraph graph, double { var hoverBold = 2.0; var grayedPen = new Pen(new SolidColorBrush(Colors.Gray, 0.4), Models.CommitGraph.Pens[0].Thickness); - var onlyHighlightCurrentBranch = OnlyHighlightCurrentBranch; - var highlightSelectedLineage = HighlightSelectedLineage; + var highlightMode = HighlightMode; + var onlyHighlightCurrentBranch = highlightMode == Models.CommitGraphHighlighting.CurrentBranchOnly || + highlightMode == Models.CommitGraphHighlighting.CurrentBranchAndSelectedLineage; + var highlightSelectedLineage = highlightMode == Models.CommitGraphHighlighting.SelectedLineageOnly || + highlightMode == Models.CommitGraphHighlighting.CurrentBranchAndSelectedLineage; var selectedLineageCommits = SelectedLineageCommits; var hoveredLineage = HoveredLineageCommits; @@ -195,7 +188,15 @@ private void DrawCurves(DrawingContext context, Models.CommitGraph graph, double hoveredLineage[link.EndCommitIndex]; var pen = link.Color < 0 ? grayedPen : Models.CommitGraph.Pens[link.Color]; - if (onlyHighlightCurrentBranch && !link.IsMerged && !isLinkInSelectedLineage) + bool shouldDim = false; + if (highlightMode == Models.CommitGraphHighlighting.CurrentBranchOnly) + shouldDim = !link.IsMerged; + else if (highlightMode == Models.CommitGraphHighlighting.SelectedLineageOnly) + shouldDim = !isLinkInSelectedLineage; + else if (highlightMode == Models.CommitGraphHighlighting.CurrentBranchAndSelectedLineage) + shouldDim = !link.IsMerged && !isLinkInSelectedLineage; + + if (shouldDim) pen = grayedPen; if (isLinkInHoveredLineage) @@ -230,7 +231,15 @@ private void DrawCurves(DrawingContext context, Models.CommitGraph graph, double var geo = new StreamGeometry(); var pen = line.Color < 0 ? grayedPen : Models.CommitGraph.Pens[line.Color]; - if (onlyHighlightCurrentBranch && !line.IsMerged && !isLineInSelectedLineage) + bool shouldDim = false; + if (highlightMode == Models.CommitGraphHighlighting.CurrentBranchOnly) + shouldDim = !line.IsMerged; + else if (highlightMode == Models.CommitGraphHighlighting.SelectedLineageOnly) + shouldDim = !isLineInSelectedLineage; + else if (highlightMode == Models.CommitGraphHighlighting.CurrentBranchAndSelectedLineage) + shouldDim = !line.IsMerged && !isLineInSelectedLineage; + + if (shouldDim) pen = grayedPen; if (line.IsHoveredRelated) @@ -297,8 +306,11 @@ private void DrawAnchors(DrawingContext context, Models.CommitGraph graph, doubl var dotFill = DotBrush; var dotFillPen = new Pen(dotFill, 2); var grayedPen = new Pen(Brushes.Gray, Models.CommitGraph.Pens[0].Thickness); - var onlyHighlightCurrentBranch = OnlyHighlightCurrentBranch; - var highlightSelectedLineage = HighlightSelectedLineage; + var highlightMode = HighlightMode; + var onlyHighlightCurrentBranch = highlightMode == Models.CommitGraphHighlighting.CurrentBranchOnly || + highlightMode == Models.CommitGraphHighlighting.CurrentBranchAndSelectedLineage; + var highlightSelectedLineage = highlightMode == Models.CommitGraphHighlighting.SelectedLineageOnly || + highlightMode == Models.CommitGraphHighlighting.CurrentBranchAndSelectedLineage; var selectedLineageCommits = SelectedLineageCommits; if (DataContext is not ViewModels.Histories vm) @@ -317,7 +329,15 @@ private void DrawAnchors(DrawingContext context, Models.CommitGraph graph, doubl bool isDotInSelectedLineage = highlightSelectedLineage && selectedLineageCommits != null && i >= 0 && i < selectedLineageCommits.Length && selectedLineageCommits[i]; var pen = Models.CommitGraph.Pens[dot.Color]; - if (onlyHighlightCurrentBranch && !dot.IsMerged && !isDotInSelectedLineage) + bool shouldDim = false; + if (highlightMode == Models.CommitGraphHighlighting.CurrentBranchOnly) + shouldDim = !dot.IsMerged; + else if (highlightMode == Models.CommitGraphHighlighting.SelectedLineageOnly) + shouldDim = !isDotInSelectedLineage; + else if (highlightMode == Models.CommitGraphHighlighting.CurrentBranchAndSelectedLineage) + shouldDim = !dot.IsMerged && !isDotInSelectedLineage; + + if (shouldDim) pen = grayedPen; switch (dot.Type) @@ -339,3 +359,4 @@ private void DrawAnchors(DrawingContext context, Models.CommitGraph graph, doubl } } } + diff --git a/src/Views/Histories.axaml b/src/Views/Histories.axaml index c9d93e6bb..9ec40ac6f 100644 --- a/src/Views/Histories.axaml +++ b/src/Views/Histories.axaml @@ -100,7 +100,14 @@ - + + + + @@ -128,7 +135,7 @@ - + @@ -233,12 +240,11 @@ Margin="0,24,0,0" Graph="{Binding Graph}" DotBrush="{DynamicResource Brush.Contents}" - OnlyHighlightCurrentBranch="{Binding $parent[v:Histories].OnlyHighlightCurrentBranch}" + HighlightMode="{Binding $parent[v:Histories].CommitGraphHighlighting}" HoveredCommitIndex="{Binding HoveredCommitIndex}" HoveredLineageCommits="{Binding HoveredLineageCommits}" SelectedLineageCommits="{Binding SelectedLineageCommits}" SelectedLineagePaths="{Binding SelectedLineagePaths}" - HighlightSelectedLineage="{Binding $parent[v:Histories].HighlightSelectedLineage}" HorizontalAlignment="Stretch" VerticalAlignment="Stretch" IsHitTestVisible="False" ClipToBounds="True"/> diff --git a/src/Views/Histories.axaml.cs b/src/Views/Histories.axaml.cs index 9eb82fc5e..c95ef9f6b 100644 --- a/src/Views/Histories.axaml.cs +++ b/src/Views/Histories.axaml.cs @@ -343,22 +343,13 @@ public AvaloniaList IssueTrackers set => SetValue(IssueTrackersProperty, value); } - public static readonly StyledProperty OnlyHighlightCurrentBranchProperty = - AvaloniaProperty.Register(nameof(OnlyHighlightCurrentBranch), true); + public static readonly StyledProperty CommitGraphHighlightingProperty = + AvaloniaProperty.Register(nameof(CommitGraphHighlighting), Models.CommitGraphHighlighting.All); - public bool OnlyHighlightCurrentBranch + public Models.CommitGraphHighlighting CommitGraphHighlighting { - get => GetValue(OnlyHighlightCurrentBranchProperty); - set => SetValue(OnlyHighlightCurrentBranchProperty, value); - } - - public static readonly StyledProperty HighlightSelectedLineageProperty = - AvaloniaProperty.Register(nameof(HighlightSelectedLineage), false); - - public bool HighlightSelectedLineage - { - get => GetValue(HighlightSelectedLineageProperty); - set => SetValue(HighlightSelectedLineageProperty, value); + get => GetValue(CommitGraphHighlightingProperty); + set => SetValue(CommitGraphHighlightingProperty, value); } public static readonly StyledProperty IsScrollToTopVisibleProperty = @@ -470,6 +461,48 @@ public async Task GotoChild() } } + private void OnHighlightsClicked(object sender, RoutedEventArgs e) + { + if (DataContext is not ViewModels.Histories vm || sender is not Control button) + return; + + var all = new MenuItem(); + all.Header = App.Text("Histories.Header.Highlights.All"); + all.Icon = vm.CommitGraphHighlighting == Models.CommitGraphHighlighting.All ? this.CreateMenuIcon("Icons.Check") : null; + all.Click += (_, _) => vm.CommitGraphHighlighting = Models.CommitGraphHighlighting.All; + + var currentBranchOnly = new MenuItem(); + currentBranchOnly.Header = App.Text("Histories.Header.Highlights.CurrentBranchOnly"); + currentBranchOnly.Icon = vm.CommitGraphHighlighting == Models.CommitGraphHighlighting.CurrentBranchOnly ? this.CreateMenuIcon("Icons.Check") : null; + currentBranchOnly.Click += (_, _) => vm.CommitGraphHighlighting = Models.CommitGraphHighlighting.CurrentBranchOnly; + + var selectedCommitsOnly = new MenuItem(); + selectedCommitsOnly.Header = App.Text("Histories.Header.Highlights.SelectedLineageOnly"); + selectedCommitsOnly.Icon = vm.CommitGraphHighlighting == Models.CommitGraphHighlighting.SelectedLineageOnly ? this.CreateMenuIcon("Icons.Check") : null; + selectedCommitsOnly.Click += (_, _) => vm.CommitGraphHighlighting = Models.CommitGraphHighlighting.SelectedLineageOnly; + + var currentBranchAndSelectedCommits = new MenuItem(); + currentBranchAndSelectedCommits.Header = App.Text("Histories.Header.Highlights.CurrentBranchAndSelectedLineage"); + currentBranchAndSelectedCommits.Icon = vm.CommitGraphHighlighting == Models.CommitGraphHighlighting.CurrentBranchAndSelectedLineage ? this.CreateMenuIcon("Icons.Check") : null; + currentBranchAndSelectedCommits.Click += (_, _) => vm.CommitGraphHighlighting = Models.CommitGraphHighlighting.CurrentBranchAndSelectedLineage; + + var menu = new ContextMenu(); + menu.Placement = PlacementMode.BottomEdgeAlignedLeft; + + var modeHeader = new MenuItem(); + modeHeader.Header = new TextBlock() { Text = App.Text("Histories.Header.Highlights"), FontWeight = FontWeight.Bold }; + modeHeader.IsEnabled = false; + menu.Items.Add(modeHeader); + + menu.Items.Add(all); + menu.Items.Add(currentBranchOnly); + menu.Items.Add(selectedCommitsOnly); + menu.Items.Add(currentBranchAndSelectedCommits); + menu.Open(button); + + e.Handled = true; + } + private void OnCommitListLayoutUpdated(object _1, EventArgs _2) { if (!IsLoaded) diff --git a/src/Views/Repository.axaml b/src/Views/Repository.axaml index c7ef8c134..bbdf42b1c 100644 --- a/src/Views/Repository.axaml +++ b/src/Views/Repository.axaml @@ -93,27 +93,11 @@ - + - - - - - - -