import { EventEmitter } from "events"; import { BoundFlags, Bounds, boundsSize, union, XY, moveBounds, boundsPos } from "./common"; import { Widget, BaseWidget } from "./widget"; import { WindowWidget } from "./window"; export interface LayoutCellData { sticky: BoundFlags; pos: XY; size: XY; } export class LayoutCell { public bounds?: Bounds; constructor(public grid: Grid, public content: Widget, public layoutData: LayoutCellData) {} getBounds(): Bounds { if (!this.bounds) this.grid.update(); return JSON.parse(JSON.stringify(this.bounds!)); } getSize(): XY { if (!this.bounds) this.grid.update(); return boundsSize(this.bounds!); } isColumn(x: number) { return this.layoutData.pos.x <= x && x < this.layoutData.pos.x + this.layoutData.size.x; } isRow(y: number) { return this.layoutData.pos.y <= y && y < this.layoutData.pos.y + this.layoutData.size.y; } minSizeX() { return this.content.minSizeX(); } minSizeY() { return this.content.minSizeY(); } getCellBounds(): Bounds { return { left: this.layoutData.pos.x, top: this.layoutData.pos.y, right: this.layoutData.pos.x + this.layoutData.size.x, bottom: this.layoutData.pos.y + this.layoutData.size.y, }; } } export class Grid extends EventEmitter { public tiles: Map = new Map(); public cells: Set = new Set(); public cellBounds: Bounds = { left: Infinity, top: Infinity, right: -Infinity, bottom: -Infinity }; public pixelBounds: Bounds = { left: Infinity, top: Infinity, right: -Infinity, bottom: -Infinity }; public columns: Set = new Set(); public rows: Set = new Set(); public columnSizes: Map = new Map(); public rowSizes: Map = new Map(); constructor() { super(); } hashPos(x: number, y: number) { return ''+x + '|' + y; } cellAt(x: number, y: number) { return this.tiles.get(this.hashPos(x, y)); } allWidth() { let padding = 0; if (this instanceof WindowWidget) { if (this.data.padding) padding = this.data.padding.left + this.data.padding.right; } if (this instanceof Widget) { if (this.data.padding) padding = this.data.padding.left + this.data.padding.right; } if (this instanceof WindowWidget) return this.data.size.x - padding; else if (this instanceof Widget) { return this.cell!.getSize().x - padding; } return 0; } allHeight() { let padding = 0; if (this instanceof WindowWidget) { if (this.data.padding) padding = this.data.padding.top + this.data.padding.bottom; } if (this instanceof Widget) { if (this.data.padding) padding = this.data.padding.top + this.data.padding.bottom; } if (this instanceof WindowWidget) { return this.data.size.y - padding; } else if (this instanceof Widget) { return this.cell!.getSize().y - padding; } return 0; } columnSize(x: number, debug: boolean = false) { if (this.columnSizes.has(x)) return this.columnSizes.get(x)!; let cc = Array.from(this.cells).filter((c) => c.isColumn(x)); let res = 0; if (cc) { res = Math.max(0, ...cc.map((c) => { let w = this.allWidth(); let d = this.columns.size; let r: number; let rc = Array.from(this.cells).filter((a) => a.layoutData.pos.y >= c.layoutData.pos.y && a.layoutData.pos.y + a.layoutData.size.y <= c.layoutData.pos.y + c.layoutData.size.y); if (c.content.data.layout.shrink) r = c.minSizeX(); else { rc.forEach((a) => { if (a.content.data.layout.shrink && d > 1) { w -= a.content.minSizeX(); d--; } }) r = Math.max(w / d, c.minSizeX()); } return r; })); } this.columnSizes.set(x, res); return res; } columnMinSize(x: number, debug: boolean = false) { let cc = Array.from(this.cells).filter((c) => c.isColumn(x)); let res = 0; if (cc) { res = Math.max(0, ...cc.map((c) => { let r = c.minSizeX() / c.layoutData.size.x; return r; })); } return res; } rowSize(y: number) { if (this.rowSizes.has(y)) return this.rowSizes.get(y)!; let cc = Array.from(this.cells).filter((c) => c.isRow(y)); let res = 0; if (cc) { res = Math.max(0, ...cc.map((c) => { let w = this.allHeight(); let d = this.rows.size; let r: number; let rc = Array.from(this.cells).filter((a) => a.layoutData.pos.x >= c.layoutData.pos.x && a.layoutData.pos.x + a.layoutData.size.x <= c.layoutData.pos.x + c.layoutData.size.x); if (c.content.data.layout.shrink) r = c.minSizeY(); else { rc.forEach((a) => { if (a.content.data.layout.shrink && d > 1) { w -= a.content.minSizeY(); d--; } }) r = Math.max(w / d, c.minSizeY()); } return r; })); } this.rowSizes.set(y, res); return res; } rowMinSize(y: number) { let rc = Array.from(this.cells).filter((c) => c.isRow(y)); let res = 0; if (rc) { res = Math.max(0, this.allHeight() / this.rows.size, ...rc.map((c) => c.minSizeY() / c.layoutData.size.y)); } return res; } addAsCell(widget: Widget) { if (widget.cell != null) return; let layout = widget.data.layout; let sticky = layout.sticky; let pos = layout.cellPos; let size = layout.cellSize; let cell = new LayoutCell(this, widget, { sticky: sticky, pos: pos, size: size }); this.cells.add(cell); for (let x = pos.x; x < pos.x + size.x; x++) { this.columns.add(x); for (let y = pos.y; y < pos.y + size.y; y++) { this.tiles.set(this.hashPos(x, y), cell); } } for (let y = pos.y; y < pos.y + size.y; y++) { this.rows.add(y); } widget.cell = cell; this.cellBounds = union(this.cellBounds, cell.getCellBounds()); this.update(); this.pixelBounds = union(this.pixelBounds, cell.getBounds()); return cell; } remove(cell: LayoutCell) { this.tiles.forEach((c, key) => { if (c === cell) this.tiles.delete(key); }); this.cells.delete(cell); this.update(); } gridMinWidth() { return Array.from(this.columns).map((c) => this.columnMinSize(c)).reduce((a, b) => a + b, 0); } gridMinHeight() { return Array.from(this.rows).map((r) => this.rowMinSize(r)).reduce((a, b) => a + b, 0); } update() { this._update(); this.cells.forEach((c) => { c.content.update(); }); } padding(): Bounds { return { left: 0, top: 0, right: 0, bottom: 0 }; } calculateCellSizes() { this.columnSizes = new Map(); this.rowSizes = new Map(); let pad = this.padding(); let x = pad.left; let y = pad.top; let cols = new Map(); let rows = new Map(); Array.from(this.columns).sort((a, b) => a - b).forEach((col) => { let sz = this.columnSize(col); cols.set(col, { a: x, b: x += sz }); }); Array.from(this.rows).sort((a, b) => a - b).forEach((row) => { let sz = this.rowSize(row); rows.set(row, { a: y, b: y += sz }); }); this.cells.forEach((c) => { let x1 = c.layoutData.pos.x; let x2 = c.layoutData.pos.x + c.layoutData.size.x - 1; let y1 = c.layoutData.pos.y; let y2 = c.layoutData.pos.y + c.layoutData.size.y - 1; c.bounds = { left: cols.get(x1)!.a, top: rows.get(y1)!.a, right: cols.get(x2)!.b, bottom: rows.get(y2)!.b, }; }); } _update() { this.calculateCellSizes(); while (true) { if (!Array.from(this.cells).some((c) => c.content.larger())) break; this.cells.forEach((c) => { c.content.growCell(); }); this.calculateCellSizes(); } this.cells.forEach((c) => { c.content.cellResized(); }); } }