Skip to content

Instantly share code, notes, and snippets.

@monyschuk
Created June 11, 2022 08:44
Show Gist options
  • Save monyschuk/21308e194297f232c1c58a3167eed5a5 to your computer and use it in GitHub Desktop.
Save monyschuk/21308e194297f232c1c58a3167eed5a5 to your computer and use it in GitHub Desktop.
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)
}
}
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