Forge
csharpdeeb25a2
1using Avalonia;
2using Avalonia.Controls;
3using Avalonia.Media;
4
5namespace CascadeIDE.Views.UiKit;
6
7/// <summary>
8/// Minimal ECAM-like dial gauge: arc + ticks + needle.
9/// Text/digital readouts are composed outside (see reference image).
10/// </summary>
11public sealed class EcamDialGauge : Control
12{
13 public static readonly StyledProperty<double> MinimumProperty =
14 AvaloniaProperty.Register<EcamDialGauge, double>(nameof(Minimum), 0);
15
16 public static readonly StyledProperty<double> MaximumProperty =
17 AvaloniaProperty.Register<EcamDialGauge, double>(nameof(Maximum), 1);
18
19 public static readonly StyledProperty<double> ValueProperty =
20 AvaloniaProperty.Register<EcamDialGauge, double>(nameof(Value), 0);
21
22 /// <summary>Angle where arc starts (degrees). Default matches ECAM-ish top arc.</summary>
23 public static readonly StyledProperty<double> StartAngleDegProperty =
24 AvaloniaProperty.Register<EcamDialGauge, double>(nameof(StartAngleDeg), -210);
25
26 /// <summary>Arc sweep (degrees).</summary>
27 public static readonly StyledProperty<double> SweepAngleDegProperty =
28 AvaloniaProperty.Register<EcamDialGauge, double>(nameof(SweepAngleDeg), 240);
29
30 public static readonly StyledProperty<int> TickCountProperty =
31 AvaloniaProperty.Register<EcamDialGauge, int>(nameof(TickCount), 6);
32
33 public static readonly StyledProperty<IBrush> ArcBrushProperty =
34 AvaloniaProperty.Register<EcamDialGauge, IBrush>(nameof(ArcBrush), Brushes.LightGray);
35
36 public static readonly StyledProperty<IBrush> TickBrushProperty =
37 AvaloniaProperty.Register<EcamDialGauge, IBrush>(nameof(TickBrush), Brushes.LightGray);
38
39 public static readonly StyledProperty<IBrush> NeedleBrushProperty =
40 AvaloniaProperty.Register<EcamDialGauge, IBrush>(nameof(NeedleBrush), new SolidColorBrush(Color.Parse("#00FF6A")));
41
42 public static readonly StyledProperty<IBrush> LimitBrushProperty =
43 AvaloniaProperty.Register<EcamDialGauge, IBrush>(nameof(LimitBrush), new SolidColorBrush(Color.Parse("#FFB300")));
44
45 /// <summary>Optional limit marker value (draws a small orange tick on the arc).</summary>
46 public static readonly StyledProperty<double?> LimitMarkerValueProperty =
47 AvaloniaProperty.Register<EcamDialGauge, double?>(nameof(LimitMarkerValue));
48
49 public double Minimum { get => GetValue(MinimumProperty); set => SetValue(MinimumProperty, value); }
50 public double Maximum { get => GetValue(MaximumProperty); set => SetValue(MaximumProperty, value); }
51 public double Value { get => GetValue(ValueProperty); set => SetValue(ValueProperty, value); }
52 public double StartAngleDeg { get => GetValue(StartAngleDegProperty); set => SetValue(StartAngleDegProperty, value); }
53 public double SweepAngleDeg { get => GetValue(SweepAngleDegProperty); set => SetValue(SweepAngleDegProperty, value); }
54 public int TickCount { get => GetValue(TickCountProperty); set => SetValue(TickCountProperty, value); }
55 public IBrush ArcBrush { get => GetValue(ArcBrushProperty); set => SetValue(ArcBrushProperty, value); }
56 public IBrush TickBrush { get => GetValue(TickBrushProperty); set => SetValue(TickBrushProperty, value); }
57 public IBrush NeedleBrush { get => GetValue(NeedleBrushProperty); set => SetValue(NeedleBrushProperty, value); }
58 public IBrush LimitBrush { get => GetValue(LimitBrushProperty); set => SetValue(LimitBrushProperty, value); }
59 public double? LimitMarkerValue { get => GetValue(LimitMarkerValueProperty); set => SetValue(LimitMarkerValueProperty, value); }
60
61 public override void Render(DrawingContext context)
62 {
63 base.Render(context);
64
65 var bounds = Bounds;
66 if (bounds.Width <= 1 || bounds.Height <= 1)
67 return;
68
69 var stroke = Math.Max(1, Math.Min(bounds.Width, bounds.Height) * 0.03);
70 var tick = Math.Max(1, stroke * 0.9);
71
72 var size = Math.Min(bounds.Width, bounds.Height);
73 var center = new Point(bounds.X + bounds.Width / 2, bounds.Y + bounds.Height / 2);
74 var radius = size * 0.42;
75
76 var arcPen = new Pen(ArcBrush, stroke, lineCap: PenLineCap.Round);
77 var tickPen = new Pen(TickBrush, tick, lineCap: PenLineCap.Round);
78 var needlePen = new Pen(NeedleBrush, stroke * 1.15, lineCap: PenLineCap.Round);
79
80 // Arc
81 var start = StartAngleDeg;
82 var end = StartAngleDeg + SweepAngleDeg;
83 context.DrawGeometry(null, arcPen, BuildArc(center, radius, start, end));
84
85 // Ticks
86 var ticks = Math.Max(2, TickCount);
87 for (var i = 0; i < ticks; i++)
88 {
89 var t = ticks == 1 ? 0 : (double)i / (ticks - 1);
90 var a = Lerp(start, end, t);
91 var p0 = Polar(center, radius * 0.92, a);
92 var p1 = Polar(center, radius * 1.02, a);
93 context.DrawLine(tickPen, p0, p1);
94 }
95
96 // Optional limit marker
97 if (LimitMarkerValue is { } lim && Maximum > Minimum)
98 {
99 var lt = Math.Clamp((lim - Minimum) / (Maximum - Minimum), 0, 1);
100 var a = Lerp(start, end, lt);
101 var p0 = Polar(center, radius * 0.88, a);
102 var p1 = Polar(center, radius * 1.08, a);
103 var limPen = new Pen(LimitBrush, tick * 1.2, lineCap: PenLineCap.Round);
104 context.DrawLine(limPen, p0, p1);
105 }
106
107 // Needle
108 var v = Value;
109 var max = Maximum;
110 var min = Minimum;
111 var vt = max > min ? Math.Clamp((v - min) / (max - min), 0, 1) : 0;
112 var va = Lerp(start, end, vt);
113 var needleEnd = Polar(center, radius * 0.95, va);
114 context.DrawLine(needlePen, center, needleEnd);
115
116 // Small hub
117 context.DrawEllipse(NeedleBrush, null, center, stroke * 0.55, stroke * 0.55);
118 }
119
120 private static Geometry BuildArc(Point c, double r, double startDeg, double endDeg)
121 {
122 var g = new StreamGeometry();
123 using var ctx = g.Open();
124 var p0 = Polar(c, r, startDeg);
125 var p1 = Polar(c, r, endDeg);
126 var sweep = Math.Abs(endDeg - startDeg);
127 var isLarge = sweep >= 180;
128 ctx.BeginFigure(p0, isFilled: false);
129 ctx.ArcTo(
130 p1,
131 new Size(r, r),
132 rotationAngle: 0,
133 isLargeArc: isLarge,
134 sweepDirection: endDeg >= startDeg ? SweepDirection.Clockwise : SweepDirection.CounterClockwise);
135 ctx.EndFigure(false);
136 return g;
137 }
138
139 private static Point Polar(Point c, double r, double deg)
140 {
141 var rad = deg * (Math.PI / 180.0);
142 return new Point(c.X + r * Math.Cos(rad), c.Y + r * Math.Sin(rad));
143 }
144
145 private static double Lerp(double a, double b, double t) => a + (b - a) * t;
146}
147
148
View only · write via MCP/CIDE