Created
June 11, 2022 08:44
-
-
Save monyschuk/21308e194297f232c1c58a3167eed5a5 to your computer and use it in GitHub Desktop.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
import Foundation | |
#if canImport(Cocoa) | |
import Cocoa | |
#elseif canImport(UIKit) | |
import UIKit | |
#endif | |
public struct EdgeInsets { | |
var top, bottom, leading, trailing: CGFloat | |
static var zero: EdgeInsets { | |
return .init(top: 0, bottom: 0, leading: 0, trailing: 0) | |
} | |
} | |
/// A space-efficient grid definition | |
public struct Grid { | |
struct Span { | |
var min, len: CGFloat | |
var max: CGFloat { return min + len } | |
} | |
private struct RunList { | |
struct Run { | |
var offset: CGFloat | |
var range: Range<Int> | |
var cell, divider: CGFloat | |
var isFinal: Bool | |
var length: CGFloat { | |
let count = CGFloat(range.count) | |
return cell * count + divider * (isFinal ? count - 1 : count) | |
} | |
func contains(_ index: Int) -> Bool { | |
return range.contains(index) | |
} | |
func contains(_ position: CGFloat) -> Bool { | |
return (offset..<offset+length).contains(position) | |
} | |
init(offset: CGFloat, range: Range<Int>, cell: CGFloat, divider: CGFloat, isFinal: Bool = false) { | |
self.offset = offset; self.range = range; self.cell = cell; self.divider = divider; self.isFinal = isFinal | |
} | |
} | |
var count: Int | |
var leading, cell, divider, trailing: CGFloat | |
var custom: [(Int, CGFloat, CGFloat)] = [] { | |
didSet { | |
self.custom = self.custom.sorted { $0.0 < $1.0 } | |
} | |
} | |
private(set) var runs: [Run] = [] | |
private func rebuildRuns() -> [Run] { | |
var offset = leading | |
if count == 0 { | |
return [] | |
} else if custom.isEmpty { | |
return [Run(offset: offset, range: 0..<count, cell: cell, divider: divider, isFinal: true)] | |
} else { | |
let prefix: [Run] = custom.reduce(into: []) { list, tuple in | |
let idx = tuple.0 | |
let clen = tuple.1 | |
let dlen = tuple.2 | |
guard idx < count else { | |
return | |
} | |
let pidx = list.last?.range.upperBound ?? 0 | |
guard pidx <= idx else { | |
return | |
} | |
if pidx == idx { | |
let next = Run(offset: offset, range: idx..<idx + 1, cell: clen, divider: dlen) | |
offset = next.offset + next.length | |
list.append(next) | |
} else { | |
let prev = Run(offset: offset, range: pidx..<idx, cell: cell, divider: divider) | |
let next = Run(offset: offset + prev.length, range: idx..<idx + 1, cell: clen, divider: dlen) | |
offset = next.offset + next.length | |
list.append(prev) | |
list.append(next) | |
} | |
} | |
let suffix: [Run] = custom.last!.0 == count - 1 | |
? [] | |
: [Run(offset: offset, range: custom.last!.0 + 1 ..< count, cell: cell, divider: divider, isFinal: true)] | |
return prefix + suffix | |
} | |
} | |
var length: CGFloat { | |
return leading + runs.reduce(0) { $0 + $1.length } + trailing | |
} | |
func span(at index: Int) -> (index: Int, cell: Span, divider: Span)? { | |
return runs | |
.first { $0.contains(index) } | |
.flatMap { | |
let stride = $0.cell + $0.divider | |
let count = CGFloat(index - $0.range.lowerBound) | |
let cell = Span(min: $0.offset + count * stride, len: $0.cell) | |
let divider = Span(min: cell.max, len: $0.divider) | |
return (index: index, cell: cell, divider: divider) | |
} | |
} | |
func span(at position: CGFloat) -> (index: Int, cell: Span, divider: Span)? { | |
return runs | |
.first { $0.contains(position) } | |
.flatMap { | |
let stride = $0.cell + $0.divider | |
let count = floor((position - $0.offset) / stride) | |
let cell = Span(min: $0.offset + count * stride, len: $0.cell) | |
let divider = Span(min: cell.max, len: $0.divider) | |
return (index: $0.range.lowerBound + Int(count), cell: cell, divider: divider) | |
} | |
} | |
func spans(in range: CountableClosedRange<Int>) -> [(index: Int, cell: Span, divider: Span)] { | |
return range.lazy.map { self.span(at: $0) }.compactMap { $0 } | |
} | |
func spans(in range: ClosedRange<CGFloat>) -> [(index: Int, cell: Span, divider: Span)] { | |
if runs.isEmpty { return [] } | |
let lb = max(range.lowerBound, runs.first!.offset) | |
let ub = min(range.upperBound, runs.last!.offset + runs.last!.length - 0.01) | |
if let s0 = span(at: lb), let s1 = span(at: ub) { | |
return spans(in: s0.index...s1.index) | |
} else { | |
return [] | |
} | |
} | |
mutating func append(cells: Int) { | |
count += cells | |
runs = rebuildRuns() | |
} | |
mutating func insert(cells: Int, at index: Int) { | |
count += cells | |
custom = self.custom.map { tuple in | |
tuple.0 < index | |
? tuple | |
: (tuple.0 + cells, tuple.1, tuple.2) | |
} | |
runs = rebuildRuns() | |
} | |
private func cellSize(at index: Int) -> CGFloat { | |
return custom.first(where: { $0.0 == index }).flatMap { $0.1 } ?? cell | |
} | |
private func dividerSize(at index: Int) -> CGFloat { | |
return custom.first(where: { $0.0 == index }).flatMap { $0.2 } ?? divider | |
} | |
mutating func resizeCell(_ size: CGFloat, at index: Int) { | |
custom = custom.filter({ $0.0 != index }) + [(index, size, dividerSize(at: index))] | |
runs = rebuildRuns() | |
} | |
mutating func resizeDivider(_ size: CGFloat, at index: Int) { | |
custom = custom.filter({ $0.0 != index }) + [(index, cellSize(at: index), size)] | |
runs = rebuildRuns() | |
} | |
init(count: Int, cell: CGFloat, divider: CGFloat, leading: CGFloat, trailing: CGFloat) { | |
self.count = count; self.cell = cell; self.divider = divider; self.leading = leading; self.trailing = trailing; self.runs = rebuildRuns() | |
} | |
} | |
private var rows, cols: RunList | |
var numberOfRows: Int { | |
return rows.count | |
} | |
var numberOfColumns: Int { | |
return cols.count | |
} | |
mutating func appendRows(_ count: Int) { | |
rows.append(cells: count) | |
} | |
mutating func appendColumns(_ count: Int) { | |
cols.append(cells: count) | |
} | |
mutating func insertRows(_ count: Int, at index: Int) { | |
rows.insert(cells: count, at: index) | |
} | |
mutating func insertColumns(_ count: Int, at index: Int) { | |
cols.insert(cells: count, at: index) | |
} | |
mutating func resize(row: Int, size: CGFloat) { | |
rows.resizeCell(size, at: row) | |
} | |
mutating func resize(column: Int, size: CGFloat) { | |
cols.resizeCell(size, at: column) | |
} | |
mutating func resize(rowDivider: Int, size: CGFloat) { | |
rows.resizeDivider(size, at: rowDivider) | |
} | |
mutating func resize(columnDivider: Int, size: CGFloat) { | |
cols.resizeDivider(size, at: columnDivider) | |
} | |
var size: CGSize { | |
return CGSize(width: cols.length + cols.leading + cols.trailing, height: rows.length + rows.leading + rows.trailing) | |
} | |
func rows(in rect: CGRect) -> [(index: Int, cell: Span, divider: Span)] { | |
return rows.spans(in: rect.minY...rect.maxY) | |
} | |
func columns(in rect: CGRect) -> [(index: Int, cell: Span, divider: Span)] { | |
return cols.spans(in: rect.minX...rect.maxX) | |
} | |
typealias BorderVisitor = (CGRect)->() | |
typealias DividerVisitor = (Int, CGRect)->() | |
typealias CellVisitor = (Int, Int, CGRect)->() | |
func visit(rect: CGRect, borders: BorderVisitor?, rowDividers: DividerVisitor?, columnDividers: DividerVisitor?, cells: CellVisitor?) -> Void { | |
let top = rows.leading | |
let left = cols.leading | |
let right = cols.trailing | |
let bottom = rows.trailing | |
let width = cols.length - left - right | |
let height = rows.length - top - bottom | |
let gridRect = CGRect(x: 0, y: 0, width: width + left + right, height: height + top + bottom) | |
if let visitor = borders { | |
let ts = gridRect.divided(atDistance: top, from: .minYEdge).slice | |
let bs = gridRect.divided(atDistance: bottom, from: .maxYEdge).slice | |
let ls = gridRect.divided(atDistance: left, from: .minXEdge).slice | |
let rs = gridRect.divided(atDistance: right, from: .maxXEdge).slice | |
if ts.intersects(rect) { visitor(ts) } | |
if bs.intersects(rect) { visitor(bs) } | |
if ls.intersects(rect) { visitor(ls) } | |
if rs.intersects(rect) { visitor(rs) } | |
} | |
if cells != nil || rowDividers != nil || columnDividers != nil { | |
let rs = rows(in: rect) | |
let cs = columns(in: rect) | |
for r in rs { | |
let rdr = CGRect(x: left, y: r.divider.min, width: width, height: r.divider.len) | |
rowDividers?(r.index, rdr) | |
for c in cs { | |
let cdr = CGRect(x: c.divider.min, y: top, width: c.divider.len, height: height) | |
columnDividers?(c.index, cdr) | |
let cr = CGRect(x: c.cell.min, y: r.cell.min, width: c.cell.len, height: r.cell.len) | |
cells?(r.index, c.index, cr) | |
} | |
} | |
} | |
} | |
init(rows: Int, cols: Int, cellSize: CGSize, intercellSpacing: CGSize, border: EdgeInsets) { | |
self.rows = RunList(count: rows, cell: cellSize.height, divider: intercellSpacing.height, leading: border.top, trailing: border.bottom) | |
self.cols = RunList(count: cols, cell: cellSize.width, divider: intercellSpacing.width, leading: border.leading, trailing: border.trailing) | |
} | |
} | |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
import SwiftUI | |
struct GridView: View { | |
private var grid: Grid = Grid( | |
rows: 1_000, | |
cols: 1_000, | |
cellSize: .init( | |
width: 72, | |
height: 24 | |
), | |
intercellSpacing: .init( | |
width: 1, | |
height: 1 | |
), | |
border: .init( | |
top: 2, | |
bottom: 2, | |
leading: 2, | |
trailing: 2 | |
) | |
) | |
var body: some View { | |
Canvas { ctx, size in | |
let dirty = ctx.clipBoundingRect | |
ctx.fill(Path(dirty), with: .color(Color(white: 1))) | |
let c = ctx | |
func fill(_ rect: CGRect, _ white: CGFloat) { | |
c.fill(Path(rect), with: .color(Color(white: white))) | |
} | |
func drawBorder(_ rect: CGRect) { | |
fill(rect, 0.25) | |
} | |
func drawDivider(_ i: Int, _ rect: CGRect) { | |
fill(rect, 0.75) | |
} | |
func drawCell(_ r: Int, _ c: Int, _ rect: CGRect) { | |
fill(rect, 0.95) | |
} | |
grid.visit( | |
rect: dirty, | |
borders: drawBorder, | |
rowDividers: drawDivider, | |
columnDividers: drawDivider, | |
cells: drawCell | |
) | |
} | |
.frame( | |
width: grid.size.width, | |
height: grid.size.height | |
) | |
} | |
} | |
struct GridView_Previews: PreviewProvider { | |
static var previews: some View { | |
GridView() | |
} | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment