Draw depot couplers as an icon and a link bar in the state's colour

Each state gets its own icon and colour, and a bar under it ties the two
cards: broken when free, thicker when permanent, absent at the back. Green
is gone - on the strip it already means selected or turned round.
This commit is contained in:
maj00r
2026-10-04 19:09:57 +02:00
parent 8c046cfed4
commit 526ff91b3b
2 changed files with 47 additions and 34 deletions

View File

@@ -1,6 +1,7 @@
using System.Collections.Generic;
using System.Linq;
using Avalonia.Media;
using Material.Icons;
using StarterNG.Classes;
namespace StarterNG.Views;
@@ -26,14 +27,21 @@ public enum CouplerState
NoEndSignals
}
/// <summary>How a coupler is drawn on the consist strip, so its state reads at a glance.</summary>
/// <summary>
/// How a coupler is drawn on the consist strip, so its state reads at a glance:
/// an icon and a bar under it that ties the two cards, both in the state's colour.
/// Green is left out - on the strip it means selected or turned round.
/// </summary>
public static class CouplerLook
{
private static readonly IBrush Dim = new SolidColorBrush(Color.Parse("#808080"));
private static readonly IBrush Green = new SolidColorBrush(Color.Parse("#41C400"));
private static readonly IBrush Blue = new SolidColorBrush(Color.Parse("#4AA3FF"));
private static readonly IBrush Red = new SolidColorBrush(Color.Parse("#E5484D"));
private static readonly IBrush Amber = new SolidColorBrush(Color.Parse("#E0A030"));
private static readonly IBrush Steel = Solid("#C3CBD3");
private static readonly IBrush Dim = Solid("#6B737B");
private static readonly IBrush Sky = Solid("#4FB8FF");
private static readonly IBrush Violet = Solid("#B08CFF");
private static readonly IBrush Red = Solid("#FF5A5A");
private static readonly IBrush Amber = Solid("#F0A030");
private static IBrush Solid(string color) => new SolidColorBrush(Color.Parse(color));
public static CouplerState StateOf(Coupling coupling, bool trailing)
{
@@ -47,26 +55,31 @@ public static class CouplerLook
return (flags & Coupling.ControlMU) != 0 ? CouplerState.MultipleUnit : CouplerState.Coupled;
}
// Box drawing and a plain disc: Windows draws no colour emoji for these.
public static string Glyph(CouplerState state) => state switch
public static MaterialIconKind Icon(CouplerState state) => state switch
{
CouplerState.Free => "╎",
CouplerState.Permanent => "═",
CouplerState.EndSignals => "●",
_ => "≣"
CouplerState.Free => MaterialIconKind.LinkVariantOff,
CouplerState.Permanent => MaterialIconKind.LinkLock,
CouplerState.EndSignals => MaterialIconKind.RecordCircle,
CouplerState.NoEndSignals => MaterialIconKind.AlertCircleOutline,
_ => MaterialIconKind.LinkVariant
};
/// <summary>The colour of the glyph; null keeps the ordinary text colour.</summary>
public static IBrush? Brush(CouplerState state) => state switch
public static IBrush Brush(CouplerState state) => state switch
{
CouplerState.Free => Dim,
CouplerState.MultipleUnit => Green,
CouplerState.Permanent => Blue,
CouplerState.MultipleUnit => Sky,
CouplerState.Permanent => Violet,
CouplerState.EndSignals => Red,
CouplerState.NoEndSignals => Amber,
_ => null
_ => Steel
};
/// <summary>The bar under the icon: none at the back, broken where nothing is coupled.</summary>
public static bool HasBar(CouplerState state) => state is not (CouplerState.EndSignals or CouplerState.NoEndSignals);
/// <summary>Thicker where the vehicles do not part in shunting.</summary>
public static double BarThickness(CouplerState state) => state == CouplerState.Permanent ? 5 : 3;
/// <summary>What the coupler connects, by the names the coupler editor uses.</summary>
public static IReadOnlyList<string> Connections(Coupling coupling) =>
Enumerable.Range(0, CouplingBits.BitKeys.Length)

View File

@@ -2,7 +2,9 @@ using System;
using System.Collections.Generic;
using System.Linq;
using Avalonia;
using Avalonia.Collections;
using Avalonia.Controls;
using Avalonia.Controls.Shapes;
using Avalonia.Input;
using Avalonia.Layout;
using Avalonia.Media;
@@ -413,36 +415,34 @@ public sealed class VehicleCards
public Control BuildCoupler(ConsistItem item, bool trailing = false)
{
var coupling = Consist.TailCar(item).Coupling;
var glyph = new TextBlock
var icon = new MaterialIcon { Width = 18, Height = 18, HorizontalAlignment = HorizontalAlignment.Center };
var bar = new Line
{
FontSize = 18,
VerticalAlignment = VerticalAlignment.Center,
HorizontalAlignment = HorizontalAlignment.Center
StartPoint = new Point(0, 0),
EndPoint = new Point(22, 0),
HorizontalAlignment = HorizontalAlignment.Center,
StrokeLineCap = PenLineCap.Round
};
var coupler = new Button
{
Padding = new Thickness(4, 6, 4, 6),
Padding = new Thickness(3, 6, 3, 6),
MinWidth = 0,
Cursor = _hand,
VerticalAlignment = VerticalAlignment.Center,
Content = glyph
Content = new StackPanel { Spacing = 4, Children = { icon, bar } }
};
coupler.Classes.Add("Basic");
void Restyle()
{
var state = CouplerLook.StateOf(coupling, trailing);
glyph.Text = CouplerLook.Glyph(state);
if (CouplerLook.Brush(state) is { } brush)
{
glyph.Foreground = brush;
glyph.Opacity = 1;
}
else
{
glyph.ClearValue(TextBlock.ForegroundProperty);
glyph.Opacity = 0.7;
}
var brush = CouplerLook.Brush(state);
icon.Kind = CouplerLook.Icon(state);
icon.Foreground = brush;
bar.Opacity = CouplerLook.HasBar(state) ? 1 : 0; // kept in the layout, so every icon sits level
bar.Stroke = brush;
bar.StrokeThickness = CouplerLook.BarThickness(state);
bar.StrokeDashArray = state == CouplerState.Free ? new AvaloniaList<double> { 1.5, 1.5 } : null;
ToolTip.SetTip(coupler, CouplerLook.Describe(coupling, trailing));
}