uicore-ts
Version:
UICore is a library to build native-like user interfaces using pure Typescript. No HTML is needed at all. Components are described as TS classes and all user interactions are handled explicitly. This library is strongly inspired by the UIKit framework tha
662 lines (465 loc) • 17.9 kB
text/typescript
import { FIRST_OR_NIL, IS, IS_NIL, IS_NOT_NIL, nil, NO, UIObject, YES } from "./UIObject"
import { UIPoint } from "./UIPoint"
import { UIView } from "./UIView"
export class UIRectangle extends UIObject {
_isBeingUpdated: boolean
rectanglePointDidChange?: (b: any) => void
max: UIPoint
min: UIPoint
constructor(x: number = 0, y: number = 0, height: number = 0, width: number = 0) {
super()
this.min = new UIPoint(Number.POSITIVE_INFINITY, Number.POSITIVE_INFINITY)
this.max = new UIPoint(Number.NEGATIVE_INFINITY, Number.NEGATIVE_INFINITY)
this.min.didChange = (point) => {
this.rectanglePointDidChange?.(point)
this._rectanglePointDidChange()
}
this.max.didChange = (point) => {
this.rectanglePointDidChange?.(point)
this._rectanglePointDidChange()
}
this._isBeingUpdated = NO
this.min = new UIPoint(x, y)
this.max = new UIPoint(x + width, y + height)
if (IS_NIL(height)) {
this.max.y = height
}
if (IS_NIL(width)) {
this.max.x = width
}
}
copy() {
return new UIRectangle(this.x, this.y, this.height, this.width)
}
isEqualTo(rectangle: UIRectangle | null | undefined) {
return (IS(rectangle) && this.min.isEqualTo(rectangle.min) && this.max.isEqualTo(rectangle.max))
}
static zero() {
return new UIRectangle(0, 0, 0, 0)
}
containsPoint(point: UIPoint) {
return this.min.x <= point.x && this.min.y <= point.y &&
point.x <= this.max.x && point.y <= this.max.y
}
updateByAddingPoint(point: UIPoint) {
if (!point) {
point = new UIPoint(0, 0)
}
this.beginUpdates()
const min = this.min.copy()
if (min.x === nil) {
min.x = this.max.x
}
if (min.y === nil) {
min.y = this.max.y
}
const max = this.max.copy()
if (max.x === nil) {
max.x = this.min.x
}
if (max.y === nil) {
max.y = this.min.y
}
this.min.x = Math.min(min.x, point.x)
this.min.y = Math.min(min.y, point.y)
this.max.x = Math.max(max.x, point.x)
this.max.y = Math.max(max.y, point.y)
this.finishUpdates()
}
scale(scale: number) {
if (IS_NOT_NIL(this.max.y)) {
this.height = this.height * scale
}
if (IS_NOT_NIL(this.max.x)) {
this.width = this.width * scale
}
}
get height() {
if (this.max.y === nil) {
return nil
}
return this.max.y - this.min.y
}
set height(height: number) {
this.max.y = this.min.y + height
}
get width() {
if (this.max.x === nil) {
return nil
}
return this.max.x - this.min.x
}
set width(width: number) {
this.max.x = this.min.x + width
}
get x() {
return this.min.x
}
set x(x: number) {
this.beginUpdates()
const width = this.width
this.min.x = x
this.max.x = this.min.x + width
this.finishUpdates()
}
get y() {
return this.min.y
}
set y(y: number) {
this.beginUpdates()
const height = this.height
this.min.y = y
this.max.y = this.min.y + height
this.finishUpdates()
}
get topLeft() {
return this.min.copy()
}
get topRight() {
return new UIPoint(this.max.x, this.y)
}
get bottomLeft() {
return new UIPoint(this.x, this.max.y)
}
get bottomRight() {
return this.max.copy()
}
get center() {
return this.min.copy().add(this.min.to(this.max).scale(0.5))
}
set center(center: UIPoint) {
const offset = this.center.to(center)
this.offsetByPoint(offset)
}
offsetByPoint(offset: UIPoint) {
this.min.add(offset)
this.max.add(offset)
return this
}
concatenateWithRectangle(rectangle: UIRectangle) {
this.updateByAddingPoint(rectangle.bottomRight)
this.updateByAddingPoint(rectangle.topLeft)
return this
}
intersectionRectangleWithRectangle(rectangle: UIRectangle): UIRectangle {
const result = this.copy()
result.beginUpdates()
const min = result.min
if (min.x === nil) {
min.x = rectangle.max.x - Math.min(result.width, rectangle.width)
}
if (min.y === nil) {
min.y = rectangle.max.y - Math.min(result.height, rectangle.height)
}
const max = result.max
if (max.x === nil) {
max.x = rectangle.min.x + Math.min(result.width, rectangle.width)
}
if (max.y === nil) {
max.y = rectangle.min.y + Math.min(result.height, rectangle.height)
}
result.min.x = Math.max(result.min.x, rectangle.min.x)
result.min.y = Math.max(result.min.y, rectangle.min.y)
result.max.x = Math.min(result.max.x, rectangle.max.x)
result.max.y = Math.min(result.max.y, rectangle.max.y)
if (result.height < 0) {
const averageY = (this.center.y + rectangle.center.y) * 0.5
result.min.y = averageY
result.max.y = averageY
}
if (result.width < 0) {
const averageX = (this.center.x + rectangle.center.x) * 0.5
result.min.x = averageX
result.max.x = averageX
}
result.finishUpdates()
return result
}
get area() {
return this.height * this.width
}
intersectsWithRectangle(rectangle: UIRectangle) {
return (this.intersectionRectangleWithRectangle(rectangle).area != 0)
}
// add some space around the rectangle
rectangleWithInsets(left: number, right: number, bottom: number, top: number) {
const result = this.copy()
result.min.x = this.min.x + left
result.max.x = this.max.x - right
result.min.y = this.min.y + top
result.max.y = this.max.y - bottom
return result
}
rectangleWithInset(inset: number) {
return this.rectangleWithInsets(inset, inset, inset, inset)
}
rectangleWithHeight(height: number, centeredOnPosition: number = nil) {
if (isNaN(centeredOnPosition)) {
centeredOnPosition = nil
}
const result = this.copy()
result.height = height
if (centeredOnPosition != nil) {
const change = height - this.height
result.offsetByPoint(new UIPoint(0, change * centeredOnPosition).scale(-1))
}
return result
}
rectangleWithWidth(width: number, centeredOnPosition: number = nil) {
if (isNaN(centeredOnPosition)) {
centeredOnPosition = nil
}
const result = this.copy()
result.width = width
if (centeredOnPosition != nil) {
const change = width - this.width
result.offsetByPoint(new UIPoint(change * centeredOnPosition, 0).scale(-1))
}
return result
}
rectangleWithHeightRelativeToWidth(heightRatio: number = 1, centeredOnPosition: number = nil) {
return this.rectangleWithHeight(this.width * heightRatio, centeredOnPosition)
}
rectangleWithWidthRelativeToHeight(widthRatio: number = 1, centeredOnPosition: number = nil) {
return this.rectangleWithWidth(this.height * widthRatio, centeredOnPosition)
}
rectangleWithX(x: number, centeredOnPosition: number = 0) {
const result = this.copy()
result.x = x - result.width * centeredOnPosition
return result
}
rectangleWithY(y: number, centeredOnPosition: number = 0) {
const result = this.copy()
result.y = y - result.height * centeredOnPosition
return result
}
rectangleByAddingX(x: number) {
const result = this.copy()
result.x = this.x + x
return result
}
rectangleByAddingY(y: number) {
const result = this.copy()
result.y = this.y + y
return result
}
rectangleWithRelativeValues(
relativeXPosition: number,
widthMultiplier: number,
relativeYPosition: number,
heightMultiplier: number
) {
const result = this.copy()
const width = result.width
const height = result.height
result.width = widthMultiplier * width
result.height = heightMultiplier * height
result.center = new UIPoint(
relativeXPosition * width,
relativeYPosition * height
)
return result
}
rectanglesBySplittingWidth(
weights: number[],
paddings: number | number[] = 0,
absoluteWidths: number | number[] = nil
) {
if (IS_NIL(paddings)) {
paddings = 1
}
if (!((paddings as any) instanceof Array)) {
paddings = [paddings].arrayByRepeating(weights.length - 1)
}
paddings = (paddings as number[]).arrayByTrimmingToLengthIfLonger(weights.length - 1)
if (!(absoluteWidths instanceof Array) && IS_NOT_NIL(absoluteWidths)) {
absoluteWidths = [absoluteWidths].arrayByRepeating(weights.length)
}
const result: UIRectangle[] = []
const sumOfWeights = weights.reduce(
(a, b, index) => {
if (IS_NOT_NIL((absoluteWidths as number[])[index])) {
b = 0
}
return a + b
},
0
)
const sumOfPaddings = paddings.summedValue
const sumOfAbsoluteWidths = (absoluteWidths as number[]).summedValue
const totalRelativeWidth = this.width - sumOfPaddings - sumOfAbsoluteWidths
let previousCellMaxX = this.x
for (let i = 0; i < weights.length; i++) {
let resultWidth: number
if (IS_NOT_NIL(absoluteWidths[i])) {
resultWidth = absoluteWidths[i] || 0
}
else {
resultWidth = totalRelativeWidth * (weights[i] / sumOfWeights)
}
const rectangle = this.rectangleWithWidth(resultWidth)
let padding = 0
if (paddings.length > i && paddings[i]) {
padding = paddings[i]
}
rectangle.x = previousCellMaxX
previousCellMaxX = rectangle.max.x + padding
result.push(rectangle)
}
return result
}
rectanglesBySplittingHeight(
weights: number[],
paddings: number | number[] = 0,
absoluteHeights: number | number[] = nil
) {
if (IS_NIL(paddings)) {
paddings = 1
}
if (!((paddings as any) instanceof Array)) {
paddings = [paddings].arrayByRepeating(weights.length - 1)
}
paddings = (paddings as number[]).arrayByTrimmingToLengthIfLonger(weights.length - 1)
if (!(absoluteHeights instanceof Array) && IS_NOT_NIL(absoluteHeights)) {
absoluteHeights = [absoluteHeights].arrayByRepeating(weights.length)
}
const result: UIRectangle[] = []
const sumOfWeights = weights.reduce(
(a, b, index) => {
if (IS_NOT_NIL((absoluteHeights as number[])[index])) {
b = 0
}
return a + b
},
0
)
const sumOfPaddings = paddings.summedValue
const sumOfAbsoluteHeights = (absoluteHeights as number[]).summedValue
const totalRelativeHeight = this.height - sumOfPaddings - sumOfAbsoluteHeights
var previousCellMaxY = this.y
for (var i = 0; i < weights.length; i++) {
var resultHeight: number
if (IS_NOT_NIL(absoluteHeights[i])) {
resultHeight = absoluteHeights[i] || 0
}
else {
resultHeight = totalRelativeHeight * (weights[i] / sumOfWeights)
}
const rectangle = this.rectangleWithHeight(resultHeight)
var padding = 0
if (paddings.length > i && paddings[i]) {
padding = paddings[i]
}
rectangle.y = previousCellMaxY
previousCellMaxY = rectangle.max.y + padding
//rectangle = rectangle.rectangleWithInsets(0, 0, padding, 0);
result.push(rectangle)
}
return result
}
rectanglesByEquallySplittingWidth(numberOfFrames: number, padding: number = 0) {
const result: UIRectangle[] = []
const totalPadding = padding * (numberOfFrames - 1)
const resultWidth = (this.width - totalPadding) / numberOfFrames
for (var i = 0; i < numberOfFrames; i++) {
const rectangle = this.rectangleWithWidth(resultWidth, i / (numberOfFrames - 1))
result.push(rectangle)
}
return result
}
rectanglesByEquallySplittingHeight(numberOfFrames: number, padding: number = 0) {
const result: UIRectangle[] = []
const totalPadding = padding * (numberOfFrames - 1)
const resultHeight = (this.height - totalPadding) / numberOfFrames
for (var i = 0; i < numberOfFrames; i++) {
const rectangle = this.rectangleWithHeight(resultHeight, i / (numberOfFrames - 1))
result.push(rectangle)
}
return result
}
distributeViewsAlongWidth(
views: UIView[],
weights: number | number[] = 1,
paddings?: number | number[],
absoluteWidths?: number | number[]
) {
if (!(weights instanceof Array)) {
weights = [weights].arrayByRepeating(views.length)
}
const frames = this.rectanglesBySplittingWidth(weights, paddings, absoluteWidths)
frames.forEach((frame, index) => FIRST_OR_NIL(views[index]).frame = frame)
return this
}
distributeViewsAlongHeight(
views: UIView[],
weights: number | number[] = 1,
paddings?: number | number[],
absoluteHeights?: number | number[]
) {
if (!(weights instanceof Array)) {
weights = [weights].arrayByRepeating(views.length)
}
const frames = this.rectanglesBySplittingHeight(weights, paddings, absoluteHeights)
frames.forEach((frame, index) => FIRST_OR_NIL(views[index]).frame = frame)
return this
}
distributeViewsEquallyAlongWidth(views: UIView[], padding: number) {
const frames = this.rectanglesByEquallySplittingWidth(views.length, padding)
frames.forEach((frame, index) => views[index].frame = frame)
return this
}
distributeViewsEquallyAlongHeight(views: UIView[], padding: number) {
const frames = this.rectanglesByEquallySplittingHeight(views.length, padding)
frames.forEach((frame, index) => views[index].frame = frame)
return this
}
rectangleForNextRow(padding: number = 0, height: number | ((constrainingWidth: number) => number) = this.height) {
if (height instanceof Function) {
height = height(this.width)
}
const result = this.rectangleWithY(this.max.y + padding)
if (height != this.height) {
result.height = height
}
return result
}
rectangleForNextColumn(padding: number = 0, width: number | ((constrainingHeight: number) => number) = this.width) {
if (width instanceof Function) {
width = width(this.height)
}
const result = this.rectangleWithX(this.max.x + padding)
if (width != this.width) {
result.width = width
}
return result
}
rectangleForPreviousRow(padding: number = 0) {
return this.rectangleWithY(this.min.y - this.height - padding)
}
rectangleForPreviousColumn(padding: number = 0) {
return this.rectangleWithX(this.min.x - this.width - padding)
}
// Bounding box
static boundingBoxForPoints(points: string | any[]) {
const result = new UIRectangle()
for (let i = 0; i < points.length; i++) {
result.updateByAddingPoint(points[i])
}
return result
}
beginUpdates() {
this._isBeingUpdated = YES
}
finishUpdates() {
this._isBeingUpdated = NO
this.didChange()
}
didChange() {
// Callback to be set by delegate
}
_rectanglePointDidChange() {
if (!this._isBeingUpdated) {
this.didChange()
}
}
}