| 1 | #nullable enable |
| 2 | |
| 3 | namespace CascadeIDE.Views.SkiaKit; |
| 4 | |
| 5 | /// <summary>Раскладка плиток в сетке для Skia-surfaces (картотека тем, graph cards, …).</summary> |
| 6 | internal readonly record struct SkiaTileGridOptions( |
| 7 | float MinTileWidth = 200f, |
| 8 | float GapX = 12f, |
| 9 | float GapY = 12f, |
| 10 | float OriginX = 12f, |
| 11 | int MaxColumns = 4); |
| 12 | |
| 13 | internal readonly record struct SkiaTilePlacement(float Left, float Top, float Width); |
| 14 | |
| 15 | internal static class SkiaTileGridLayout |
| 16 | { |
| 17 | public static int ComputeColumnCount(float contentWidth, in SkiaTileGridOptions options) |
| 18 | { |
| 19 | if (contentWidth <= options.MinTileWidth) |
| 20 | return 1; |
| 21 | var columns = (int)((contentWidth + options.GapX) / (options.MinTileWidth + options.GapX)); |
| 22 | return Math.Clamp(columns, 1, options.MaxColumns); |
| 23 | } |
| 24 | |
| 25 | public static float TileWidth(float contentWidth, int columns, in SkiaTileGridOptions options) => |
| 26 | (contentWidth - options.GapX * (columns - 1)) / columns; |
| 27 | |
| 28 | /// <summary> |
| 29 | /// Раскладывает элементы в сетку; <paramref name="measureHeight"/> вызывается с шириной плитки. |
| 30 | /// Возвращает новую Y после блока и список размещений (порядок совпадает с <paramref name="items"/>). |
| 31 | /// </summary> |
| 32 | public static (float NextY, IReadOnlyList<SkiaTilePlacement> Placements) Layout<T>( |
| 33 | IReadOnlyList<T> items, |
| 34 | float contentWidth, |
| 35 | float startY, |
| 36 | in SkiaTileGridOptions options, |
| 37 | Func<T, float, float> measureHeight) |
| 38 | { |
| 39 | if (items.Count == 0) |
| 40 | return (startY, Array.Empty<SkiaTilePlacement>()); |
| 41 | |
| 42 | var columns = ComputeColumnCount(contentWidth, in options); |
| 43 | var tileWidth = TileWidth(contentWidth, columns, in options); |
| 44 | var placements = new SkiaTilePlacement[items.Count]; |
| 45 | var col = 0; |
| 46 | var rowStartY = startY; |
| 47 | var rowMaxHeight = 0f; |
| 48 | |
| 49 | for (var i = 0; i < items.Count; i++) |
| 50 | { |
| 51 | var height = measureHeight(items[i], tileWidth); |
| 52 | var left = options.OriginX + col * (tileWidth + options.GapX); |
| 53 | placements[i] = new SkiaTilePlacement(left, rowStartY, tileWidth); |
| 54 | rowMaxHeight = Math.Max(rowMaxHeight, height); |
| 55 | col++; |
| 56 | if (col < columns) |
| 57 | continue; |
| 58 | |
| 59 | col = 0; |
| 60 | rowStartY += rowMaxHeight + options.GapY; |
| 61 | rowMaxHeight = 0f; |
| 62 | } |
| 63 | |
| 64 | if (col > 0) |
| 65 | rowStartY += rowMaxHeight; |
| 66 | |
| 67 | return (rowStartY + options.GapY, placements); |
| 68 | } |
| 69 | } |
| 70 | |