From d427a6e16080a63160ab0a756625a4cacc6dcaec Mon Sep 17 00:00:00 2001 From: BruceChen Date: Sun, 29 Mar 2026 19:53:31 +0800 Subject: [PATCH] Add tooltip service and integrate with MinimapControl - Introduced TuiTooltipService for managing tooltips across TUI components. - Updated MinimapControl to utilize the new tooltip service for enhanced entity information display. - Refactored tooltip rendering logic to improve visibility and interaction based on mouse position. --- MinecraftClient/Tui/MainTuiView.cs | 9 ++ MinecraftClient/Tui/MinimapControl.cs | 144 +++++++++++------------ MinecraftClient/Tui/TuiTooltipService.cs | 114 ++++++++++++++++++ 3 files changed, 194 insertions(+), 73 deletions(-) create mode 100644 MinecraftClient/Tui/TuiTooltipService.cs diff --git a/MinecraftClient/Tui/MainTuiView.cs b/MinecraftClient/Tui/MainTuiView.cs index 1cde95e4..34ff06db 100644 --- a/MinecraftClient/Tui/MainTuiView.cs +++ b/MinecraftClient/Tui/MainTuiView.cs @@ -45,6 +45,8 @@ namespace MinecraftClient.Tui private readonly MinimapControl _minimapControl; private volatile bool _minimapVisible; + private TuiTooltipService? _tooltipService; + private readonly Border _suggestionBorder; private readonly StackPanel _suggestionPanel; private CommandSuggestion[] _suggestions = Array.Empty(); @@ -57,6 +59,8 @@ namespace MinecraftClient.Tui private int MaxVisibleSuggestions => Math.Max(1, Settings.Config.Console.CommandSuggestion.Max_Displayed_Suggestions); + public TuiTooltipService? TooltipService => _tooltipService; + public MainTuiView() { Background = Brushes.Black; @@ -194,6 +198,10 @@ namespace MinecraftClient.Tui Children = { _mainContent, _minimapBorder, _notificationBorder, _suggestionBorder } }; + _tooltipService = new TuiTooltipService(_rootPanel); + _minimapControl.TooltipService = _tooltipService; + _minimapControl.Position = mmCfg.Position; + Content = _rootPanel; if (mmCfg.Enabled) @@ -1083,6 +1091,7 @@ namespace MinecraftClient.Tui _minimapBorder.HorizontalAlignment = hAlign; _minimapBorder.VerticalAlignment = vAlign; _minimapBorder.Margin = margin; + _minimapControl.Position = pos; Settings.Config.Console.Minimap.Position = pos; } diff --git a/MinecraftClient/Tui/MinimapControl.cs b/MinecraftClient/Tui/MinimapControl.cs index 1e93c390..33f25465 100644 --- a/MinecraftClient/Tui/MinimapControl.cs +++ b/MinecraftClient/Tui/MinimapControl.cs @@ -45,12 +45,11 @@ namespace MinecraftClient.Tui private readonly Grid _mapGrid; private readonly DispatcherTimer _timer; - private readonly Canvas _tooltipCanvas; - private readonly Border _tooltipBorder; - private readonly StackPanel _tooltipContent; private SampleResult? _lastResult; private int _hoverCol = -1; private int _hoverRow = -1; + private double _hoverGlobalX; + private double _hoverGlobalY; public int BlocksPerPixel { @@ -60,6 +59,10 @@ namespace MinecraftClient.Tui public NameDisplayConfig NameConfig => _nameConfig; + public TuiTooltipService? TooltipService { get; set; } + + public MinimapPosition Position { get; set; } = MinimapPosition.top_right; + public int MapPixelWidth => _mapWidth; public int MapPixelHeight => _mapHeight; @@ -83,33 +86,10 @@ namespace MinecraftClient.Tui _infoRow = new StackPanel { Orientation = Orientation.Horizontal }; _legendPanel = new StackPanel { Orientation = Orientation.Horizontal }; - _tooltipContent = new StackPanel { Orientation = Orientation.Vertical }; - _tooltipBorder = new Border - { - Background = new SolidColorBrush(Color.FromArgb(230, 20, 20, 20)), - BorderBrush = new SolidColorBrush(Color.FromRgb(120, 120, 120)), - BorderThickness = new Thickness(1), - Padding = new Thickness(1), - Child = _tooltipContent, - IsVisible = false, - }; - - _tooltipCanvas = new Canvas - { - IsHitTestVisible = false, - Children = { _tooltipBorder }, - }; - - var mapLayer = new Panel - { - ClipToBounds = true, - Children = { _mapGrid, _tooltipCanvas }, - }; - var root = new StackPanel { Orientation = Orientation.Vertical, - Children = { mapLayer, _infoRow, _legendPanel }, + Children = { _mapGrid, _infoRow, _legendPanel }, }; Content = root; @@ -236,6 +216,7 @@ namespace MinecraftClient.Tui { public string Name = ""; public MobCategory Category; + public double X, Y, Z; public float Health; public float MaxHealth; public int Priority; @@ -352,6 +333,9 @@ namespace MinecraftClient.Tui { Name = eName, Category = cat, + X = entity.Location.X, + Y = entity.Location.Y, + Z = entity.Location.Z, Health = entity.Health, MaxHealth = -1, Priority = priority, @@ -379,6 +363,9 @@ namespace MinecraftClient.Tui { Name = client.GetUsername(), Category = MobCategory.Player, + X = playerLoc.X, + Y = playerLoc.Y, + Z = playerLoc.Z, Health = client.GetHealth(), MaxHealth = 20f, Priority = 5, @@ -700,6 +687,19 @@ namespace MinecraftClient.Tui _hoverCol = col; _hoverRow = row; + + if (this.VisualRoot is Visual root + && _mapGrid.TranslatePoint(pos, root) is { } gp) + { + _hoverGlobalX = gp.X; + _hoverGlobalY = gp.Y; + } + else + { + _hoverGlobalX = pos.X; + _hoverGlobalY = pos.Y; + } + UpdateTooltip(col, row); } @@ -712,13 +712,14 @@ namespace MinecraftClient.Tui { _hoverCol = -1; _hoverRow = -1; - _tooltipBorder.IsVisible = false; + TooltipService?.Hide(); } private void UpdateTooltip(int col, int row) { + var svc = TooltipService; var result = _lastResult; - if (result is null) { _tooltipBorder.IsVisible = false; return; } + if (svc is null || result is null) { svc?.Hide(); return; } int bpp = result.Bpp; int centerX = result.CenterX; @@ -731,7 +732,7 @@ namespace MinecraftClient.Tui int baseZ_top = result.PlayerBlockZ + (topPixelY - centerY) * bpp; int baseZ_bot = result.PlayerBlockZ + (botPixelY - centerY) * bpp; - _tooltipContent.Children.Clear(); + var lines = new List(); if (bpp == 1) { @@ -741,7 +742,7 @@ namespace MinecraftClient.Tui string coordLine = baseZ_top == baseZ_bot ? $"{baseX}, {surfY_top}, {baseZ_top}" : $"{baseX}, {surfY_top}, {baseZ_top} / {baseX}, {surfY_bot}, {baseZ_bot}"; - _tooltipContent.Children.Add(MakeTooltipText(coordLine, Brushes.White)); + lines.Add(new TuiTooltipLine { Text = coordLine, Foreground = Brushes.White }); if (result.BlockTypes is not null) { @@ -751,7 +752,7 @@ namespace MinecraftClient.Tui string blockLine = mat_top == mat_bot ? FormatMaterialName(mat_top) : $"{FormatMaterialName(mat_top)} / {FormatMaterialName(mat_bot)}"; - _tooltipContent.Children.Add(MakeTooltipText(blockLine, Brushes.LightGray)); + lines.Add(new TuiTooltipLine { Text = blockLine, Foreground = Brushes.LightGray }); } } else @@ -759,33 +760,40 @@ namespace MinecraftClient.Tui int endX = baseX + bpp - 1; int endZ_bot = baseZ_bot + bpp - 1; string coordLine = $"X {baseX}~{endX} Z {baseZ_top}~{endZ_bot}"; - _tooltipContent.Children.Add(MakeTooltipText(coordLine, Brushes.White)); + lines.Add(new TuiTooltipLine { Text = coordLine, Foreground = Brushes.White }); - AppendBlockSummary(result, col, topPixelY, botPixelY); + AppendBlockSummaryLines(result, col, topPixelY, botPixelY, lines); } - AppendEntityInfo(result, col, topPixelY, botPixelY); + AppendEntityInfoLines(result, col, topPixelY, botPixelY, lines); - if (_tooltipContent.Children.Count == 0) + if (lines.Count == 0) { - _tooltipBorder.IsVisible = false; + svc.Hide(); return; } - int maxTipW = Math.Max(10, _mapWidth / 2 - 2); - _tooltipBorder.MaxWidth = maxTipW; - _tooltipBorder.MaxHeight = _cellRows; + bool preferRight = Position switch + { + MinimapPosition.top_left or MinimapPosition.bottom_left => true, + MinimapPosition.top_right or MinimapPosition.bottom_right => false, + _ => true, + }; - bool showRight = col < _mapWidth / 2; - int tipX = showRight ? col + 2 : Math.Max(0, col - maxTipW - 1); - int tipY = Math.Clamp(row, 0, _cellRows - 1); + double mx = _hoverGlobalX; + double my = _hoverGlobalY; - Canvas.SetLeft(_tooltipBorder, tipX); - Canvas.SetTop(_tooltipBorder, tipY); - _tooltipBorder.IsVisible = true; + if (Position == MinimapPosition.center + && this.VisualRoot is Visual root) + { + preferRight = mx < root.Bounds.Width / 2; + } + + svc.Show(mx, my, lines, preferRight); } - private void AppendBlockSummary(SampleResult result, int col, int topPy, int botPy) + private void AppendBlockSummaryLines(SampleResult result, int col, int topPy, int botPy, + List lines) { if (result.BlockSummary is null) return; @@ -797,8 +805,6 @@ namespace MinecraftClient.Tui if (merged.Count == 0) return; var sorted = merged.OrderByDescending(kv => kv.Value).Take(4); - int totalSamples = 0; - foreach (var kv in merged) totalSamples += kv.Value; var parts = new List(); foreach (var kv in sorted) @@ -811,8 +817,11 @@ namespace MinecraftClient.Tui if (parts.Count == 0) return; - string line = string.Join(", ", parts); - _tooltipContent.Children.Add(MakeTooltipText(line, Brushes.LightGray)); + lines.Add(new TuiTooltipLine + { + Text = string.Join(", ", parts), + Foreground = Brushes.LightGray, + }); } private static void MergeBlockCounts(List<(Material Mat, int Count)>?[,] summary, @@ -829,7 +838,8 @@ namespace MinecraftClient.Tui } } - private void AppendEntityInfo(SampleResult result, int col, int topPy, int botPy) + private static void AppendEntityInfoLines(SampleResult result, int col, int topPy, int botPy, + List lines) { var entityMap = result.EntityMap; if (entityMap is null) return; @@ -851,19 +861,20 @@ namespace MinecraftClient.Tui if (!seen.Add(key)) continue; var catColor = MinimapEntityClassifier.GetBaseColor(ent.Category); - string hpStr; + string coordStr = $"({ent.X:F1}, {ent.Y:F1}, {ent.Z:F1})"; + string hpStr = ""; if (ent.Health > 0) { hpStr = ent.MaxHealth > 0 - ? $" HP:{ent.Health:F0}/{ent.MaxHealth:F0}" - : $" HP:{ent.Health:F0}"; + ? $" HP:{ent.Health:F0}/{ent.MaxHealth:F0}" + : $" HP:{ent.Health:F0}"; } - else - hpStr = ""; - _tooltipContent.Children.Add(MakeTooltipText( - $"{ent.Name}{hpStr}", - new SolidColorBrush(catColor))); + lines.Add(new TuiTooltipLine + { + Text = $"{ent.Name} {coordStr}{hpStr}", + Foreground = new SolidColorBrush(catColor), + }); shown++; } } @@ -879,19 +890,6 @@ namespace MinecraftClient.Tui } } - private static TextBlock MakeTooltipText(string text, IBrush foreground) - { - return new TextBlock - { - Text = text, - Foreground = foreground, - TextWrapping = TextWrapping.Wrap, - Padding = new Thickness(0), - Margin = new Thickness(0), - FontSize = 1, - }; - } - private static string FormatMaterialName(Material mat) { if (mat == Material.Air) return "Air"; diff --git a/MinecraftClient/Tui/TuiTooltipService.cs b/MinecraftClient/Tui/TuiTooltipService.cs new file mode 100644 index 00000000..0ae51607 --- /dev/null +++ b/MinecraftClient/Tui/TuiTooltipService.cs @@ -0,0 +1,114 @@ +using System; +using System.Collections.Generic; +using Avalonia; +using Avalonia.Controls; +using Avalonia.Media; + +namespace MinecraftClient.Tui +{ + public sealed class TuiTooltipLine + { + public string Text { get; init; } = ""; + public IBrush Foreground { get; init; } = Brushes.White; + } + + /// + /// Global tooltip that floats above all TUI content. + /// Owned by MainTuiView, used by minimap / chat / other components. + /// + public sealed class TuiTooltipService + { + private readonly Panel _rootPanel; + private readonly Canvas _canvas; + private readonly Border _border; + private readonly StackPanel _content; + + internal TuiTooltipService(Panel rootPanel) + { + _content = new StackPanel { Orientation = Avalonia.Layout.Orientation.Vertical }; + _border = new Border + { + Background = new SolidColorBrush(Color.FromArgb(230, 20, 20, 20)), + BorderBrush = new SolidColorBrush(Color.FromRgb(120, 120, 120)), + BorderThickness = new Thickness(1), + Padding = new Thickness(1), + Child = _content, + IsVisible = false, + }; + + _canvas = new Canvas + { + IsHitTestVisible = false, + Children = { _border }, + }; + + _rootPanel = rootPanel; + rootPanel.Children.Add(_canvas); + } + + /// Global X of the mouse cursor. + /// Global Y of the mouse cursor. + /// + /// If true, try placing tooltip to the right of mouseX; + /// if false, try placing to the left. + /// The service auto-flips when the tooltip would overflow the screen. + /// + public void Show(double mouseX, double mouseY, IReadOnlyList lines, + bool preferRight = true) + { + _content.Children.Clear(); + + if (lines.Count == 0) + { + _border.IsVisible = false; + return; + } + + int maxChars = 0; + foreach (var line in lines) + { + _content.Children.Add(new TextBlock + { + Text = line.Text, + Foreground = line.Foreground, + TextWrapping = TextWrapping.Wrap, + Padding = new Thickness(0), + Margin = new Thickness(0), + FontSize = 1, + }); + if (line.Text.Length > maxChars) + maxChars = line.Text.Length; + } + + double tipW = maxChars + 4; + double screenW = _rootPanel.Bounds.Width; + + const double gap = 1; + double gx; + if (preferRight) + { + gx = mouseX + gap; + if (gx + tipW > screenW) + gx = mouseX - tipW - gap; + } + else + { + gx = mouseX - tipW - gap; + if (gx < 0) + gx = mouseX + gap; + } + + Canvas.SetLeft(_border, Math.Max(0, gx)); + Canvas.SetTop(_border, Math.Max(0, mouseY)); + _border.IsVisible = true; + } + + public void Hide() + { + _border.IsVisible = false; + _content.Children.Clear(); + } + + public bool IsVisible => _border.IsVisible; + } +}