Forge
csharpdeeb25a2
1using System.Collections;
2using System.Collections.Specialized;
3using Avalonia;
4using Avalonia.Controls;
5using Avalonia.Input;
6using Avalonia.Media;
7using Avalonia.Threading;
8using CascadeIDE.Cockpit.PrimitivesKit;
9using CascadeIDE.Models;
10
11namespace CascadeIDE.Views;
12
13/// <summary>
14/// Полоса ламп annunciator / Korry по коллекции <see cref="AnnunciatorLampItem"/>; метрики <see cref="AnnunciatorLampMetrics"/>, ячейки <see cref="LabeledAnnunciatorLampFace"/>; отрисовка через <see cref="DrawingContext"/>,
15/// тот же контур, что <see cref="CockpitSkiaSceneRenderer"/> и <see cref="SkiaHost"/> (ADR 0055, 0063).
16/// Без раздувания под доступную высоту — фиксированная геометрия ячеек.
17/// </summary>
18public sealed class AnnunciatorLampStrip : Control
19{
20 public static readonly StyledProperty<IEnumerable?> ItemsProperty =
21 AvaloniaProperty.Register<AnnunciatorLampStrip, IEnumerable?>(nameof(Items));
22
23 private INotifyCollectionChanged? _collectionSubscription;
24 private readonly List<(Rect Rect, AnnunciatorLampItem Item)> _hitCells = [];
25 private AnnunciatorLampItem? _hovered;
26
27 static AnnunciatorLampStrip()
28 {
29 AffectsRender<AnnunciatorLampStrip>(ItemsProperty);
30 FocusableProperty.OverrideDefaultValue<AnnunciatorLampStrip>(false);
31 }
32
33 public IEnumerable? Items
34 {
35 get => GetValue(ItemsProperty);
36 set => SetValue(ItemsProperty, value);
37 }
38
39 protected override void OnPropertyChanged(AvaloniaPropertyChangedEventArgs change)
40 {
41 base.OnPropertyChanged(change);
42 if (change.Property != ItemsProperty)
43 return;
44 UnsubscribeCollection();
45 if (change.NewValue is INotifyCollectionChanged n)
46 {
47 n.CollectionChanged += OnCollectionChanged;
48 _collectionSubscription = n;
49 }
50
51 InvalidateMeasure();
52 InvalidateVisual();
53 }
54
55 protected override void OnDetachedFromVisualTree(VisualTreeAttachmentEventArgs e)
56 {
57 base.OnDetachedFromVisualTree(e);
58 UnsubscribeCollection();
59 }
60
61 private void UnsubscribeCollection()
62 {
63 if (_collectionSubscription is not null)
64 {
65 _collectionSubscription.CollectionChanged -= OnCollectionChanged;
66 _collectionSubscription = null;
67 }
68 }
69
70 private void OnCollectionChanged(object? sender, NotifyCollectionChangedEventArgs e) =>
71 Dispatcher.UIThread.Post(
72 () =>
73 {
74 // Сначала часто приходит measure при пустой коллекции → (0,0). Без InvalidateMeasure размер не обновится, когда строки добавят асинхронно.
75 InvalidateMeasure();
76 InvalidateVisual();
77 },
78 DispatcherPriority.Normal);
79
80 protected override Size MeasureOverride(Size availableSize)
81 {
82 var items = EnumerateItems().ToList();
83 if (items.Count == 0)
84 return new Size(0, 0);
85
86 var sz = AnnunciatorLampMetrics.MeasureStrip(items.Count);
87 var aw = availableSize.Width;
88 if (double.IsNaN(aw) || double.IsInfinity(aw))
89 aw = sz.Width;
90 // Родитель может отдать ширину 0 на раннем measure — не схлопываемся до нуля, иначе Render не рисует.
91 if (aw <= 0)
92 aw = sz.Width;
93 var width = Math.Min(sz.Width, aw);
94 return new Size(width, sz.Height);
95 }
96
97 public override void Render(DrawingContext context)
98 {
99 base.Render(context);
100 _hitCells.Clear();
101
102 var items = EnumerateItems().ToList();
103 if (items.Count == 0)
104 return;
105
106 var w = Bounds.Width;
107 var h = Bounds.Height;
108 if (w <= 0 || h <= 0)
109 return;
110
111 var panel = new Rect(0, 0, w, h);
112 AnnunciatorLampMetrics.DrawPanelBackground(context, panel);
113
114 var columnsPerRow = AnnunciatorLampMetrics.DefaultStripColumns;
115 var pad = AnnunciatorLampMetrics.DefaultPanelPadding;
116 var gap = AnnunciatorLampMetrics.DefaultGap;
117 var cellW = AnnunciatorLampMetrics.DefaultCellWidth;
118 var cellH = AnnunciatorLampMetrics.DefaultCellHeight;
119
120 for (var i = 0; i < items.Count; i++)
121 {
122 var item = items[i];
123 var col = i % columnsPerRow;
124 var row = i / columnsPerRow;
125 var x = pad + col * (cellW + gap);
126 var y = pad + row * (cellH + gap);
127 var outer = new Rect(x, y, cellW, cellH);
128
129 new LabeledAnnunciatorLampFace(item.LampShortLabel, item.Level).Draw(context, outer);
130
131 _hitCells.Add((outer, item));
132 }
133 }
134
135 protected override void OnPointerMoved(PointerEventArgs e)
136 {
137 base.OnPointerMoved(e);
138 var p = e.GetPosition(this);
139 AnnunciatorLampItem? hit = null;
140 foreach (var (r, it) in _hitCells)
141 {
142 if (r.Contains(p))
143 {
144 hit = it;
145 break;
146 }
147 }
148
149 if (string.Equals(_hovered?.Id, hit?.Id, StringComparison.Ordinal))
150 return;
151
152 _hovered = hit;
153 if (hit is null)
154 {
155 ToolTip.SetTip(this, null);
156 return;
157 }
158
159 var tip = new StackPanel { Spacing = 4, MaxWidth = 420 };
160 tip.Children.Add(new TextBlock { Text = hit.Title, FontWeight = FontWeight.SemiBold, TextWrapping = Avalonia.Media.TextWrapping.Wrap });
161 tip.Children.Add(new TextBlock
162 {
163 Text = hit.Detail,
164 FontSize = 11,
165 TextWrapping = Avalonia.Media.TextWrapping.Wrap,
166 Foreground = new SolidColorBrush(CockpitPrimitivesPalette.Annunciator.TooltipDetailForeground),
167 });
168 ToolTip.SetTip(this, tip);
169 }
170
171 protected override void OnPointerExited(PointerEventArgs e)
172 {
173 base.OnPointerExited(e);
174 _hovered = null;
175 ToolTip.SetTip(this, null);
176 }
177
178 private IEnumerable<AnnunciatorLampItem> EnumerateItems()
179 {
180 if (Items is null)
181 yield break;
182 foreach (var o in Items)
183 {
184 if (o is AnnunciatorLampItem it)
185 yield return it;
186 }
187 }
188
189}
190
View only · write via MCP/CIDE