using RioJoy.Core.Calibration; using RioJoy.Core.Editing; namespace RioJoy.Tray.Editor; /// /// A boolean from the legacy [JoyStick] ini section, surfaced as a clickable /// cell on the panel (mapped to ). /// public enum JoyStickSetting { InvertX, InvertY, InvertZ, InvertRx, InvertRy, InvertRz, EnableZR, } /// One [JoyStick] toggle cell placed on the panel (absolute cells). public sealed record CalibrationCell(JoyStickSetting Setting, string Label, int Col, int Row); /// /// Draws the cockpit control panel — the functional groups from /// (MFD clusters, board columns, keypads) plus the /// encoder-gauge strip — mirroring the original Win32 RIO design. Lamp buttons are /// shaded by whether the address is assigned (Off vs Dim); keypads are neutral /// (no lamps). Shared by and offline rendering. /// public static class PanelView { public const int CellW = 66; public const int CellH = 34; public const int TopStrip = 108; // encoder-gauge band height (fits the U + Rz below it) // The [JoyStick] ini section as cells, stacked vertically in the gap between the // Throttle column (col 0) and the Secondary column (col 4): a single column at // col 2, rows 10–16, under an "Axis" title (row 9). public const int CalOriginCol = 2; public const int CalOriginRow = 9; public static IList CalibrationCells { get; } = new[] { new CalibrationCell(JoyStickSetting.InvertX, "Inv X", 2, 10), new CalibrationCell(JoyStickSetting.InvertY, "Inv Y", 2, 11), new CalibrationCell(JoyStickSetting.InvertZ, "Inv Z", 2, 12), new CalibrationCell(JoyStickSetting.InvertRx, "Inv Rx", 2, 13), new CalibrationCell(JoyStickSetting.InvertRy, "Inv Ry", 2, 14), new CalibrationCell(JoyStickSetting.InvertRz, "Inv Rz", 2, 15), new CalibrationCell(JoyStickSetting.EnableZR, "ZR mix", 2, 16), }; public static Size GridSize(IList buttons) { int maxCol = 0, maxRow = 0; foreach (PanelButton b in buttons) { if (b.Col > maxCol) maxCol = b.Col; if (b.Row > maxRow) maxRow = b.Row; } // Fit snugly: the rightmost group frame ends just past (maxCol + 1) * CellW, // so a few px of border avoids a full empty trailing column. return new Size((maxCol + 1) * CellW + 6, TopStrip + (maxRow + 2) * CellH); } private static Rectangle Cell(int col, int row) => new(col * CellW + 2, TopStrip + row * CellH + 2, CellW - 4, CellH - 4); public static void Paint( Graphics g, IList buttons, IList groups, Func label, Func function, Func lit, Func live, AxisReadout? axes, int? selected) { g.Clear(Color.FromArgb(28, 28, 28)); using var titleFont = new Font("Segoe UI", 8f, FontStyle.Bold); using var btnFont = new Font("Segoe UI", 7.5f); using var subFont = new Font("Segoe UI", 6.75f); using var groupPen = new Pen(Color.FromArgb(80, 80, 80)); DrawEncoderStrip(g, titleFont, axes); // Group frames + titles. foreach (PanelGroup grp in groups) { var frame = new Rectangle( grp.OriginCol * CellW + 1, TopStrip + grp.OriginRow * CellH + 1, grp.Cols * CellW, (grp.Rows + 1) * CellH); g.DrawRectangle(groupPen, frame); TextRenderer.DrawText(g, grp.Title, titleFont, new Rectangle(frame.X + 2, frame.Y, frame.Width - 2, CellH), Color.Gainsboro, TextFormatFlags.Left | TextFormatFlags.VerticalCenter | TextFormatFlags.EndEllipsis); } // Buttons: caption = label (top) + assigned function (bottom). const TextFormatFlags oneLine = TextFormatFlags.HorizontalCenter | TextFormatFlags.VerticalCenter | TextFormatFlags.EndEllipsis | TextFormatFlags.NoPrefix; foreach (PanelButton b in buttons) { Rectangle r = Cell(b.Col, b.Row); string? lab = label(b.Address); string? fn = function(b.Address); bool assigned = !string.IsNullOrEmpty(fn); // Three lamp states in the cell's own colour: a physically held button is // BRIGHT; a lamp configured on (IsLit) is DIM at idle (so the press still // reads brighter); everything else is OFF. bool yellow = b.Group.Title is "Secondary" or "Screen"; bool held = live(b.Address); bool litCfg = lit(b.Address); Color fill = !b.LampCapable ? (held ? Color.FromArgb(205, 228, 255) // keypad — bright blue when pressed : Color.FromArgb(150, 182, 226)) // keypad — neutral blue : yellow ? (held ? Color.FromArgb(245, 210, 60) // Secondary/Screen — bright : litCfg ? Color.FromArgb(140, 118, 38) // dim (lamp on, idle) : Color.FromArgb(70, 60, 24)) // off : (held ? Color.FromArgb(230, 70, 70) // lamp — bright (held) : litCfg ? Color.FromArgb(120, 50, 50) // dim (lamp on, idle) : Color.FromArgb(64, 40, 40)); // off using (var fb = new SolidBrush(fill)) g.FillRectangle(fb, r); Color fg = (b.LampCapable && !yellow) ? Color.White : Color.Black; bool hasLab = !string.IsNullOrEmpty(lab); if (hasLab && assigned) { TextRenderer.DrawText(g, lab, btnFont, new Rectangle(r.X, r.Y + 1, r.Width, r.Height / 2), fg, TextFormatFlags.HorizontalCenter | TextFormatFlags.Top | TextFormatFlags.EndEllipsis | TextFormatFlags.NoPrefix); TextRenderer.DrawText(g, fn, subFont, new Rectangle(r.X, r.Bottom - r.Height / 2 - 1, r.Width, r.Height / 2), fg, TextFormatFlags.HorizontalCenter | TextFormatFlags.Bottom | TextFormatFlags.EndEllipsis | TextFormatFlags.NoPrefix); } else { string single = hasLab ? lab! : assigned ? fn! : $"{b.Address:X2}"; TextRenderer.DrawText(g, single, btnFont, r, fg, oneLine); } if (b.Address == selected) { using var hi = new Pen(Color.Yellow, 2f); g.DrawRectangle(hi, r.X, r.Y, r.Width - 1, r.Height - 1); } } } private static void DrawEncoderStrip(Graphics g, Font font, AxisReadout? axes) { using var pen = new Pen(Color.FromArgb(120, 160, 120)); using var gauge = new SolidBrush(Color.FromArgb(20, 40, 20)); using var fill = new SolidBrush(Color.FromArgb(52, 128, 70)); // Centered over the Upper Middle MFD block and arranged to match the physical // cockpit: Z on the left and the X/Y stick graph on the right (both full // height). The L & R pedals and the Rz (mixed-rudder) gauge form a big "U" in // the center — L and R the full-height arms, Rz the bar joining them. const int top = 6; const int fullH = 68; // Z, X/Y and the U's arms (full height) const int bar = 30; // stroke thickness, matching Z's width / Rz bar int baseX = 6 * CellW - 100; // cluster is 200 wide; 6*CellW centers the MFD int uX = baseX + 34, uW = 82; var zBox = new Rectangle(baseX, top, bar, fullH); var lBox = new Rectangle(uX, top, bar, fullH); // left arm of the U var rBox = new Rectangle(uX + uW - bar, top, bar, fullH); // right arm of the U var rzBox = new Rectangle(uX, top + fullH, uW, bar); // bottom bar, below L & R (no overlap) var xyBox = new Rectangle(baseX + 120, top, 80, fullH); (string Label, Rectangle Box)[] boxes = { ("Z", zBox), ("L", lBox), ("R", rBox), ("Rz", rzBox), ("X / Y", xyBox), }; foreach (var (_, box) in boxes) g.FillRectangle(gauge, box); // Live values from the RIO's analog stream (null when the link is down). // Z/Rz/X/Y show the virtual-joystick outputs; L and R show the calibrated // pedal positions from before the ZR mix, which pins Rx/Ry to center — the // pedal gauges must track the physical pedals either way. if (axes is { } a) { // Z (throttle) and the L/R pedals fill bottom-up. foreach (var (box, value) in new[] { (zBox, a.Axes.Z), (lBox, a.LeftPedal), (rBox, a.RightPedal) }) { int h = (int)Math.Round(box.Height * AxisGauges.Fraction(value)); if (h > 0) g.FillRectangle(fill, box.X, box.Bottom - h, box.Width, h); } // Rz (mixed rudder) deflects left/right from the bar's center. double d = AxisGauges.Deflection(a.Axes.Rz); int cx = rzBox.X + rzBox.Width / 2; int w = (int)Math.Round(rzBox.Width / 2.0 * Math.Abs(d)); if (w > 0) g.FillRectangle(fill, d < 0 ? cx - w : cx, rzBox.Y, w, rzBox.Height); } // Reference marks: Rz center tick + X/Y crosshair (drawn even when idle). g.DrawLine(pen, rzBox.X + rzBox.Width / 2, rzBox.Y, rzBox.X + rzBox.Width / 2, rzBox.Bottom); g.DrawLine(pen, xyBox.X, xyBox.Y + xyBox.Height / 2, xyBox.Right, xyBox.Y + xyBox.Height / 2); g.DrawLine(pen, xyBox.X + xyBox.Width / 2, xyBox.Y, xyBox.X + xyBox.Width / 2, xyBox.Bottom); foreach (var (lbl, box) in boxes) { g.DrawRectangle(pen, box); TextRenderer.DrawText(g, lbl, font, box, Color.Gainsboro, TextFormatFlags.HorizontalCenter | TextFormatFlags.VerticalCenter); } // The X/Y stick dot goes on top of the crosshair and label // (0 = left/top, matching the HID axis convention). if (axes is { } live) { int px = xyBox.X + (int)Math.Round(AxisGauges.Fraction(live.Axes.X) * (xyBox.Width - 1)); int py = xyBox.Y + (int)Math.Round(AxisGauges.Fraction(live.Axes.Y) * (xyBox.Height - 1)); using var dot = new SolidBrush(Color.FromArgb(150, 230, 150)); g.FillEllipse(dot, px - 3, py - 3, 7, 7); } } public static PanelButton? HitTest(IList buttons, Point p) { foreach (PanelButton b in buttons) if (Cell(b.Col, b.Row).Contains(p)) return b; return null; } /// /// Draw the [JoyStick] toggle block. reports each /// setting's current value; "on" cells render green (active invert / ZR mix). /// public static void PaintCalibration(Graphics g, Func state) { using var titleFont = new Font("Segoe UI", 8f, FontStyle.Bold); using var cellFont = new Font("Segoe UI", 7.5f); using var framePen = new Pen(Color.FromArgb(80, 80, 80)); var frame = new Rectangle( CalOriginCol * CellW + 1, TopStrip + CalOriginRow * CellH + 1, CellW, (CalibrationCells.Count + 1) * CellH); // title row + one cell row each g.DrawRectangle(framePen, frame); TextRenderer.DrawText(g, "Axis", titleFont, new Rectangle(frame.X + 2, frame.Y, frame.Width - 2, CellH), Color.Gainsboro, TextFormatFlags.Left | TextFormatFlags.VerticalCenter | TextFormatFlags.EndEllipsis); const TextFormatFlags fmt = TextFormatFlags.HorizontalCenter | TextFormatFlags.VerticalCenter | TextFormatFlags.EndEllipsis | TextFormatFlags.NoPrefix; foreach (CalibrationCell c in CalibrationCells) { Rectangle r = Cell(c.Col, c.Row); bool on = state(c.Setting); Color fill = on ? Color.FromArgb(60, 150, 80) : Color.FromArgb(58, 64, 72); // green on / slate off using (var fb = new SolidBrush(fill)) g.FillRectangle(fb, r); TextRenderer.DrawText(g, $"{c.Label}\n{(on ? "ON" : "off")}", cellFont, r, Color.White, fmt); } } /// Hit-test the [JoyStick] toggle cells; null if the point misses. public static CalibrationCell? HitTestCalibration(Point p) { foreach (CalibrationCell c in CalibrationCells) if (Cell(c.Col, c.Row).Contains(p)) return c; return null; } }