using System; using System.Collections.Generic; using System.Linq; namespace ytLive.Models; /// Which edge a full-width bar occupies. public enum BarEdge { Top, Bottom, } /// The scoreboard's placement and line-fitting arithmetic, kept pure so both are testable /// without a renderer. /// /// Two rules from the creator (2026-10-30): /// the strip and the social bar are a COUPLED pair that own opposite edges, resolved at render time /// from what is actually visible; and a strip that does not fit SHRINKS to a floor and then wraps to /// a second line — it never scrolls, because a creator reading the screen cannot wait for the fact /// to come round. public static class ScoreboardLayout { public const double FloorFontSize = 14; public const double MaxFontSize = 24; /// Result of fitting the enabled tiles into one strip. public readonly record struct Fit(double FontSize, int LineCount, bool Shrunk) { public bool Wrapped => LineCount > 1; } /// Resolves the edge pair. When both bars are visible they take opposite edges; the /// social bar keeps its own choice and the scoreboard is pushed to the other one. When either is /// hidden the survivor keeps its preferred edge — so hiding the scoreboard frees the social bar /// to sit anywhere, exactly as the creator asked. public static (BarEdge Social, BarEdge Scoreboard) ResolveEdges( bool socialVisible, BarEdge socialPreferred, bool scoreboardVisible, BarEdge scoreboardPreferred) { if (socialVisible && scoreboardVisible) return (socialPreferred, Opposite(socialPreferred)); return (socialPreferred, scoreboardPreferred); } public static BarEdge Opposite(BarEdge edge) => edge == BarEdge.Top ? BarEdge.Bottom : BarEdge.Top; /// Fits measured tile widths (at ) into the available /// width: full size if they fit, otherwise shrink, and only if the shrink would drop below the /// legibility floor does it wrap. Widths are passed in because the renderer MEASURES them — /// guessing a character width would make the fit lie for a wide glyph or a long handle. public static Fit FitLine(IReadOnlyList tileWidths, double separatorWidth, double availableWidth, double requestedSize = MaxFontSize, double floorSize = FloorFontSize) { var widths = tileWidths ?? System.Array.Empty(); var count = widths.Count; if (count == 0 || availableWidth <= 0) return new Fit(requestedSize, 1, false); var total = widths.Sum() + separatorWidth * (count - 1); if (total <= availableWidth) return new Fit(requestedSize, 1, false); var scaled = requestedSize * (availableWidth / total); if (scaled >= floorSize) return new Fit(scaled, 1, true); // Below the floor: keep the floor and wrap. Greedy packing, measured widths scaled to the // floor size so the line breaks match what is actually drawn. var factor = floorSize / requestedSize; var lines = 1; var lineWidth = 0.0; foreach (var width in widths) { var w = width * factor; var needed = lineWidth == 0 ? w : lineWidth + separatorWidth + w; if (needed > availableWidth && lineWidth > 0) { lines++; lineWidth = w; } else { lineWidth = needed; } } return new Fit(floorSize, lines, true); } }