Telegram Web K with changes to work inside I2P
https://web.telegram.i2p/
You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
438 lines
14 KiB
438 lines
14 KiB
/* |
|
This file is part of Telegram Desktop, |
|
the official desktop application for the Telegram messaging service. |
|
For license and copyright information please follow this link: |
|
https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL |
|
*/ |
|
|
|
import accumulate from "../helpers/array/accumulate"; |
|
import clamp from "../helpers/number/clamp"; |
|
|
|
type Size = {w: number, h: number}; |
|
export type GroupMediaLayout = { |
|
geometry: { |
|
x: number, |
|
y: number, |
|
width: number, |
|
height: number |
|
}, |
|
sides: number |
|
}; |
|
type Attempt = { |
|
lineCounts: number[], |
|
heights: number[] |
|
}; |
|
export const RectPart = { |
|
None: 0, |
|
Top: 1, |
|
Right: 2, |
|
Bottom: 4, |
|
Left: 8 |
|
}; |
|
|
|
// https://github.com/telegramdesktop/tdesktop/blob/4669c07dc5335cbf4795bbbe5b0ab7c007b9aee2/Telegram/SourceFiles/ui/grouped_layout.cpp |
|
export class Layouter { |
|
private count: number; |
|
private ratios: number[]; |
|
private proportions: string; |
|
private averageRatio: number; |
|
private maxSizeRatio: number; |
|
|
|
constructor(private sizes: Size[], private maxWidth: number, private minWidth: number, private spacing: number, private maxHeight = maxWidth) { |
|
this.count = sizes.length; |
|
this.ratios = Layouter.countRatios(sizes); |
|
this.proportions = Layouter.countProportions(this.ratios); |
|
this.averageRatio = accumulate(this.ratios, 1) / this.count; // warn |
|
this.maxSizeRatio = maxWidth / this.maxHeight; |
|
} |
|
|
|
public layout(): GroupMediaLayout[] { |
|
if(!this.count) return []; |
|
//else if(this.count === 1) return this.layoutOne(); |
|
|
|
if(this.count >= 5 || this.ratios.find(r => r > 2)) { |
|
return new ComplexLayouter(this.ratios, this.averageRatio, this.maxWidth, this.minWidth, this.spacing).layout(); |
|
} |
|
|
|
if(this.count === 2) return this.layoutTwo(); |
|
else if(this.count === 3) return this.layoutThree(); |
|
return this.layoutFour(); |
|
} |
|
|
|
private layoutTwo(): ReturnType<Layouter['layout']> { |
|
if((this.proportions === "ww") |
|
&& (this.averageRatio > 1.4 * this.maxSizeRatio) |
|
&& (this.ratios[1] - this.ratios[0] < 0.2)) { |
|
return this.layoutTwoTopBottom(); |
|
} else if(this.proportions === "ww" || this.proportions === "qq") { |
|
return this.layoutTwoLeftRightEqual(); |
|
} |
|
return this.layoutTwoLeftRight(); |
|
} |
|
|
|
private layoutThree(): ReturnType<Layouter['layout']> { |
|
//console.log('layoutThree:', this); |
|
if(this.proportions[0] === 'n') { |
|
return this.layoutThreeLeftAndOther(); |
|
} |
|
return this.layoutThreeTopAndOther(); |
|
} |
|
|
|
private layoutFour(): ReturnType<Layouter['layout']> { |
|
if(this.proportions[0] === 'w') { |
|
return this.layoutFourTopAndOther(); |
|
} |
|
return this.layoutFourLeftAndOther(); |
|
} |
|
|
|
private layoutTwoTopBottom(): ReturnType<Layouter['layout']> { |
|
const width = this.maxWidth; |
|
const height = Math.round(Math.min( |
|
width / this.ratios[0], |
|
Math.min( |
|
width / this.ratios[1], |
|
(this.maxHeight - this.spacing) / 2))); |
|
|
|
return [ |
|
{ |
|
geometry: {x: 0, y: 0, width, height}, |
|
sides: RectPart.Left | RectPart.Top | RectPart.Right |
|
}, |
|
{ |
|
geometry: {x: 0, y: height + this.spacing, width, height}, |
|
sides: RectPart.Left | RectPart.Bottom | RectPart.Right |
|
}, |
|
]; |
|
} |
|
|
|
private layoutTwoLeftRightEqual(): ReturnType<Layouter['layout']> { |
|
const width = (this.maxWidth - this.spacing) / 2; |
|
const height = Math.round(Math.min( |
|
width / this.ratios[0], |
|
Math.min(width / this.ratios[1], this.maxHeight * 1))); |
|
|
|
return [ |
|
{ |
|
geometry: {x: 0, y: 0, width, height}, |
|
sides: RectPart.Top | RectPart.Left | RectPart.Bottom |
|
}, |
|
{ |
|
geometry: {x: width + this.spacing, y: 0, width, height}, |
|
sides: RectPart.Top | RectPart.Right | RectPart.Bottom |
|
}, |
|
]; |
|
} |
|
|
|
private layoutTwoLeftRight(): ReturnType<Layouter['layout']> { |
|
const minimalWidth = Math.round(this.minWidth * 1.5); |
|
const secondWidth = Math.min( |
|
Math.round(Math.max( |
|
0.4 * (this.maxWidth - this.spacing), |
|
(this.maxWidth - this.spacing) / this.ratios[0] |
|
/ (1 / this.ratios[0] + 1 / this.ratios[1]))), |
|
this.maxWidth - this.spacing - minimalWidth); |
|
const firstWidth = this.maxWidth |
|
- secondWidth |
|
- this.spacing; |
|
const height = Math.min( |
|
this.maxHeight, |
|
Math.round(Math.min( |
|
firstWidth / this.ratios[0], |
|
secondWidth / this.ratios[1]))); |
|
|
|
return [ |
|
{ |
|
geometry: {x: 0, y: 0, width: firstWidth, height}, |
|
sides: RectPart.Top | RectPart.Left | RectPart.Bottom |
|
}, |
|
{ |
|
geometry: {x: firstWidth + this.spacing, y: 0, width: secondWidth, height}, |
|
sides: RectPart.Top | RectPart.Right | RectPart.Bottom |
|
}, |
|
]; |
|
} |
|
|
|
private layoutThreeLeftAndOther(): ReturnType<Layouter['layout']> { |
|
const firstHeight = this.maxHeight; |
|
const thirdHeight = Math.round(Math.min( |
|
(this.maxHeight - this.spacing) / 2., |
|
(this.ratios[1] * (this.maxWidth - this.spacing) |
|
/ (this.ratios[2] + this.ratios[1])))); |
|
const secondHeight = firstHeight |
|
- thirdHeight |
|
- this.spacing; |
|
const rightWidth = Math.max( |
|
this.minWidth, |
|
Math.round(Math.min( |
|
(this.maxWidth - this.spacing) / 2., |
|
Math.min( |
|
thirdHeight * this.ratios[2], |
|
secondHeight * this.ratios[1])))); |
|
const leftWidth = Math.min( |
|
Math.round(firstHeight * this.ratios[0]), |
|
this.maxWidth - this.spacing - rightWidth); |
|
|
|
return [ |
|
{ |
|
geometry: {x: 0, y: 0, width: leftWidth, height: firstHeight}, |
|
sides: RectPart.Top | RectPart.Left | RectPart.Bottom |
|
}, |
|
{ |
|
geometry: {x: leftWidth + this.spacing, y: 0, width: rightWidth, height: secondHeight}, |
|
sides: RectPart.Top | RectPart.Right |
|
}, |
|
{ |
|
geometry: {x: leftWidth + this.spacing, y: secondHeight + this.spacing, width: rightWidth, height: thirdHeight}, |
|
sides: RectPart.Bottom | RectPart.Right |
|
}, |
|
]; |
|
} |
|
|
|
private layoutThreeTopAndOther(): ReturnType<Layouter['layout']> { |
|
const firstWidth = this.maxWidth; |
|
const firstHeight = Math.round(Math.min( |
|
firstWidth / this.ratios[0], |
|
(this.maxHeight - this.spacing) * 0.66)); |
|
const secondWidth = (this.maxWidth - this.spacing) / 2; |
|
const secondHeight = Math.min( |
|
this.maxHeight - firstHeight - this.spacing, |
|
Math.round(Math.min( |
|
secondWidth / this.ratios[1], |
|
secondWidth / this.ratios[2]))); |
|
const thirdWidth = firstWidth - secondWidth - this.spacing; |
|
|
|
return [ |
|
{ |
|
geometry: {x: 0, y: 0, width: firstWidth, height: firstHeight}, |
|
sides: RectPart.Left | RectPart.Top | RectPart.Right |
|
}, |
|
{ |
|
geometry: {x: 0, y: firstHeight + this.spacing, width: secondWidth, height: secondHeight}, |
|
sides: RectPart.Bottom | RectPart.Left |
|
}, |
|
{ |
|
geometry: {x: secondWidth + this.spacing, y: firstHeight + this.spacing, width: thirdWidth, height: secondHeight}, |
|
sides: RectPart.Bottom | RectPart.Right |
|
}, |
|
]; |
|
} |
|
|
|
private layoutFourTopAndOther(): ReturnType<Layouter['layout']> { |
|
const w = this.maxWidth; |
|
const h0 = Math.round(Math.min( |
|
w / this.ratios[0], |
|
(this.maxHeight - this.spacing) * 0.66)); |
|
const h = Math.round( |
|
(this.maxWidth - 2 * this.spacing) |
|
/ (this.ratios[1] + this.ratios[2] + this.ratios[3])); |
|
const w0 = Math.max( |
|
this.minWidth, |
|
Math.round(Math.min( |
|
(this.maxWidth - 2 * this.spacing) * 0.4, |
|
h * this.ratios[1]))); |
|
const w2 = Math.round(Math.max( |
|
Math.max( |
|
this.minWidth * 1., |
|
(this.maxWidth - 2 * this.spacing) * 0.33), |
|
h * this.ratios[3])); |
|
const w1 = w - w0 - w2 - 2 * this.spacing; |
|
const h1 = Math.min( |
|
this.maxHeight - h0 - this.spacing, |
|
h); |
|
|
|
return [ |
|
{ |
|
geometry: {x: 0, y: 0, width: w, height: h0}, |
|
sides: RectPart.Left | RectPart.Top | RectPart.Right |
|
}, |
|
{ |
|
geometry: {x: 0, y: h0 + this.spacing, width: w0, height: h1}, |
|
sides: RectPart.Bottom | RectPart.Left |
|
}, |
|
{ |
|
geometry: {x: w0 + this.spacing, y: h0 + this.spacing, width: w1, height: h1}, |
|
sides: RectPart.Bottom, |
|
}, |
|
{ |
|
geometry: {x: w0 + this.spacing + w1 + this.spacing, y: h0 + this.spacing, width: w2, height: h1}, |
|
sides: RectPart.Right | RectPart.Bottom |
|
}, |
|
]; |
|
} |
|
|
|
private layoutFourLeftAndOther(): ReturnType<Layouter['layout']> { |
|
const h = this.maxHeight; |
|
const w0 = Math.round(Math.min( |
|
h * this.ratios[0], |
|
(this.maxWidth - this.spacing) * 0.6)); |
|
|
|
const w = Math.round( |
|
(this.maxHeight - 2 * this.spacing) |
|
/ (1. / this.ratios[1] + 1. / this.ratios[2] + 1. / this.ratios[3]) |
|
); |
|
const h0 = Math.round(w / this.ratios[1]); |
|
const h1 = Math.round(w / this.ratios[2]); |
|
const h2 = h - h0 - h1 - 2 * this.spacing; |
|
const w1 = Math.max( |
|
this.minWidth, |
|
Math.min(this.maxWidth - w0 - this.spacing, w)); |
|
|
|
return [ |
|
{ |
|
geometry: {x: 0, y: 0, width: w0, height: h}, |
|
sides: RectPart.Top | RectPart.Left | RectPart.Bottom |
|
}, |
|
{ |
|
geometry: {x: w0 + this.spacing, y: 0, width: w1, height: h0}, |
|
sides: RectPart.Top | RectPart.Right |
|
}, |
|
{ |
|
geometry: {x: w0 + this.spacing, y: h0 + this.spacing, width: w1, height: h1}, |
|
sides: RectPart.Right |
|
}, |
|
{ |
|
geometry: {x: w0 + this.spacing, y: h0 + h1 + 2 * this.spacing, width: w1, height: h2}, |
|
sides: RectPart.Bottom | RectPart.Right |
|
}, |
|
]; |
|
} |
|
|
|
private static countRatios(sizes: Size[]) { |
|
return sizes.map(size => size.w / size.h); |
|
} |
|
|
|
private static countProportions(ratios: number[]) { |
|
return ratios.map(ratio => (ratio > 1.2) ? 'w' : (ratio < 0.8) ? 'n' : 'q').join(''); |
|
} |
|
} |
|
|
|
class ComplexLayouter { |
|
private ratios: number[]; |
|
private count: number; |
|
|
|
constructor(ratios: number[], private averageRatio: number, private maxWidth: number, private minWidth: number, private spacing: number, private maxHeight = maxWidth * 4 / 3) { |
|
this.ratios = ComplexLayouter.cropRatios(ratios, averageRatio); |
|
this.count = ratios.length; |
|
} |
|
|
|
private static cropRatios(ratios: number[], averageRatio: number) { |
|
const kMaxRatio = 2.75; |
|
const kMinRatio = 0.6667; |
|
return ratios.map(ratio => { |
|
return averageRatio > 1.1 |
|
? clamp(ratio, 1., kMaxRatio) |
|
: clamp(ratio, kMinRatio, 1.); |
|
}); |
|
} |
|
|
|
public layout(): GroupMediaLayout[] { |
|
let result = new Array<GroupMediaLayout>(this.count); |
|
|
|
let attempts: Attempt[] = []; |
|
const multiHeight = (offset: number, count: number) => { |
|
const ratios = this.ratios.slice(offset, offset + count); // warn |
|
const sum = accumulate(ratios, 0); |
|
return (this.maxWidth - (count - 1) * this.spacing) / sum; |
|
}; |
|
const pushAttempt = (lineCounts: number[]) => { |
|
let heights: number[] = []; |
|
let offset = 0; |
|
for(let count of lineCounts) { |
|
heights.push(multiHeight(offset, count)); |
|
offset += count; |
|
} |
|
attempts.push({lineCounts, heights}); // warn |
|
}; |
|
|
|
for(let first = 1; first !== this.count; ++first) { |
|
const second = this.count - first; |
|
if(first > 3 || second > 3) { |
|
continue; |
|
} |
|
pushAttempt([first, second]); |
|
} |
|
for(let first = 1; first !== this.count - 1; ++first) { |
|
for(let second = 1; second !== this.count - first; ++second) { |
|
const third = this.count - first - second; |
|
if((first > 3) |
|
|| (second > ((this.averageRatio < 0.85) ? 4 : 3)) |
|
|| (third > 3)) { |
|
continue; |
|
} |
|
pushAttempt([first, second, third]); |
|
} |
|
} |
|
for(let first = 1; first !== this.count - 1; ++first) { |
|
for(let second = 1; second !== this.count - first; ++second) { |
|
for(let third = 1; third !== this.count - first - second; ++third) { |
|
const fourth = this.count - first - second - third; |
|
if(first > 3 || second > 3 || third > 3 || fourth > 3) { |
|
continue; |
|
} |
|
pushAttempt([first, second, third, fourth]); |
|
} |
|
} |
|
} |
|
|
|
let optimalAttempt: Attempt = null; |
|
let optimalDiff = 0; |
|
for(const attempt of attempts) { |
|
const {heights, lineCounts: counts} = attempt; |
|
const lineCount = counts.length; |
|
const totalHeight = accumulate(heights, 0) |
|
+ this.spacing * (lineCount - 1); |
|
const minLineHeight = Math.min(...heights); |
|
const maxLineHeight = Math.max(...heights); |
|
const bad1 = (minLineHeight < this.minWidth) ? 1.5 : 1; |
|
const bad2 = (() => { |
|
for(let line = 1; line !== lineCount; ++line) { |
|
if(counts[line - 1] > counts[line]) { |
|
return 1.5; |
|
} |
|
} |
|
return 1.; |
|
})(); |
|
const diff = Math.abs(totalHeight - this.maxHeight) * bad1 * bad2; |
|
if(!optimalAttempt || diff < optimalDiff) { |
|
optimalAttempt = attempt; |
|
optimalDiff = diff; |
|
} |
|
} |
|
|
|
const optimalCounts = optimalAttempt.lineCounts; |
|
const optimalHeights = optimalAttempt.heights; |
|
const rowCount = optimalCounts.length; |
|
|
|
let index = 0; |
|
let y = 0; |
|
for(let row = 0; row !== rowCount; ++row) { |
|
const colCount = optimalCounts[row]; |
|
const lineHeight = optimalHeights[row]; |
|
const height = Math.round(lineHeight); |
|
|
|
let x = 0; |
|
for(let col = 0; col !== colCount; ++col) { |
|
const sides = RectPart.None |
|
| (row === 0 ? RectPart.Top : RectPart.None) |
|
| (row === rowCount - 1 ? RectPart.Bottom : RectPart.None) |
|
| (col === 0 ? RectPart.Left : RectPart.None) |
|
| (col === colCount - 1 ? RectPart.Right : RectPart.None); |
|
|
|
const ratio = this.ratios[index]; |
|
const width = (col === colCount - 1) |
|
? (this.maxWidth - x) |
|
: Math.round(ratio * lineHeight); |
|
result[index] = { |
|
geometry: {x, y, width, height}, |
|
sides |
|
}; |
|
|
|
x += width + this.spacing; |
|
++index; |
|
} |
|
y += height + this.spacing; |
|
} |
|
|
|
return result; |
|
} |
|
} |