grouped_layout.cpp 16 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620
  1. /*
  2. This file is part of Telegram Desktop,
  3. the official desktop application for the Telegram messaging service.
  4. For license and copyright information please follow this link:
  5. https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
  6. */
  7. #include "ui/grouped_layout.h"
  8. namespace Ui {
  9. namespace {
  10. int Round(float64 value) {
  11. return int(base::SafeRound(value));
  12. }
  13. class Layouter {
  14. public:
  15. Layouter(
  16. const std::vector<QSize> &sizes,
  17. int maxWidth,
  18. int minWidth,
  19. int spacing);
  20. std::vector<GroupMediaLayout> layout() const;
  21. private:
  22. static std::vector<float64> CountRatios(const std::vector<QSize> &sizes);
  23. static std::string CountProportions(const std::vector<float64> &ratios);
  24. std::vector<GroupMediaLayout> layoutTwo() const;
  25. std::vector<GroupMediaLayout> layoutThree() const;
  26. std::vector<GroupMediaLayout> layoutFour() const;
  27. std::vector<GroupMediaLayout> layoutOne() const;
  28. std::vector<GroupMediaLayout> layoutTwoTopBottom() const;
  29. std::vector<GroupMediaLayout> layoutTwoLeftRightEqual() const;
  30. std::vector<GroupMediaLayout> layoutTwoLeftRight() const;
  31. std::vector<GroupMediaLayout> layoutThreeLeftAndOther() const;
  32. std::vector<GroupMediaLayout> layoutThreeTopAndOther() const;
  33. std::vector<GroupMediaLayout> layoutFourLeftAndOther() const;
  34. std::vector<GroupMediaLayout> layoutFourTopAndOther() const;
  35. const std::vector<QSize> &_sizes;
  36. const std::vector<float64> _ratios;
  37. const std::string _proportions;
  38. const int _count = 0;
  39. const int _maxWidth = 0;
  40. const int _maxHeight = 0;
  41. const int _minWidth = 0;
  42. const int _spacing = 0;
  43. const float64 _averageRatio = 1.;
  44. const float64 _maxSizeRatio = 1.;
  45. };
  46. class ComplexLayouter {
  47. public:
  48. ComplexLayouter(
  49. const std::vector<float64> &ratios,
  50. float64 averageRatio,
  51. int maxWidth,
  52. int minWidth,
  53. int spacing);
  54. std::vector<GroupMediaLayout> layout() const;
  55. private:
  56. struct Attempt {
  57. std::vector<int> lineCounts;
  58. std::vector<float64> heights;
  59. };
  60. static std::vector<float64> CropRatios(
  61. const std::vector<float64> &ratios,
  62. float64 averageRatio);
  63. const std::vector<float64> _ratios;
  64. const int _count = 0;
  65. const int _maxWidth = 0;
  66. const int _maxHeight = 0;
  67. const int _minWidth = 0;
  68. const int _spacing = 0;
  69. const float64 _averageRatio = 1.;
  70. };
  71. Layouter::Layouter(
  72. const std::vector<QSize> &sizes,
  73. int maxWidth,
  74. int minWidth,
  75. int spacing)
  76. : _sizes(sizes)
  77. , _ratios(CountRatios(_sizes))
  78. , _proportions(CountProportions(_ratios))
  79. , _count(int(_ratios.size()))
  80. // All apps currently use square max size first.
  81. // In complex case they use maxWidth * 4 / 3 as maxHeight.
  82. , _maxWidth(maxWidth)
  83. , _maxHeight(maxWidth)
  84. , _minWidth(minWidth)
  85. , _spacing(spacing)
  86. , _averageRatio(ranges::accumulate(_ratios, 1.) / _count)
  87. , _maxSizeRatio(_maxWidth / float64(_maxHeight)) {
  88. }
  89. std::vector<float64> Layouter::CountRatios(const std::vector<QSize> &sizes) {
  90. return ranges::views::all(
  91. sizes
  92. ) | ranges::views::transform([](const QSize &size) {
  93. return size.width() / float64(size.height());
  94. }) | ranges::to_vector;
  95. }
  96. std::string Layouter::CountProportions(const std::vector<float64> &ratios) {
  97. return ranges::views::all(
  98. ratios
  99. ) | ranges::views::transform([](float64 ratio) {
  100. return (ratio > 1.2) ? 'w' : (ratio < 0.8) ? 'n' : 'q';
  101. }) | ranges::to<std::string>();
  102. }
  103. std::vector<GroupMediaLayout> Layouter::layout() const {
  104. if (!_count) {
  105. return {};
  106. } else if (_count == 1) {
  107. return layoutOne();
  108. }
  109. using namespace rpl::mappers;
  110. if (_count >= 5 || ranges::find_if(_ratios, _1 > 2) != _ratios.end()) {
  111. return ComplexLayouter(
  112. _ratios,
  113. _averageRatio,
  114. _maxWidth,
  115. _minWidth,
  116. _spacing).layout();
  117. }
  118. if (_count == 2) {
  119. return layoutTwo();
  120. } else if (_count == 3) {
  121. return layoutThree();
  122. }
  123. return layoutFour();
  124. }
  125. std::vector<GroupMediaLayout> Layouter::layoutTwo() const {
  126. Expects(_count == 2);
  127. if ((_proportions == "ww")
  128. && (_averageRatio > 1.4 * _maxSizeRatio)
  129. && (_ratios[1] - _ratios[0] < 0.2)) {
  130. return layoutTwoTopBottom();
  131. } else if (_proportions == "ww" || _proportions == "qq") {
  132. return layoutTwoLeftRightEqual();
  133. }
  134. return layoutTwoLeftRight();
  135. }
  136. std::vector<GroupMediaLayout> Layouter::layoutThree() const {
  137. Expects(_count == 3);
  138. auto result = std::vector<GroupMediaLayout>(_count);
  139. if (_proportions[0] == 'n') {
  140. return layoutThreeLeftAndOther();
  141. }
  142. return layoutThreeTopAndOther();
  143. }
  144. std::vector<GroupMediaLayout> Layouter::layoutFour() const {
  145. Expects(_count == 4);
  146. auto result = std::vector<GroupMediaLayout>(_count);
  147. if (_proportions[0] == 'w') {
  148. return layoutFourTopAndOther();
  149. }
  150. return layoutFourLeftAndOther();
  151. }
  152. std::vector<GroupMediaLayout> Layouter::layoutOne() const {
  153. Expects(_count == 1);
  154. const auto width = _maxWidth;
  155. const auto height = (_sizes[0].height() * width) / _sizes[0].width();
  156. return {
  157. {
  158. QRect(0, 0, width, height),
  159. RectPart::Left | RectPart::Top | RectPart::Right | RectPart::Bottom
  160. },
  161. };
  162. }
  163. std::vector<GroupMediaLayout> Layouter::layoutTwoTopBottom() const {
  164. Expects(_count == 2);
  165. const auto width = _maxWidth;
  166. const auto height = Round(std::min(
  167. width / _ratios[0],
  168. std::min(
  169. width / _ratios[1],
  170. (_maxHeight - _spacing) / 2.)));
  171. return {
  172. {
  173. QRect(0, 0, width, height),
  174. RectPart::Left | RectPart::Top | RectPart::Right
  175. },
  176. {
  177. QRect(0, height + _spacing, width, height),
  178. RectPart::Left | RectPart::Bottom | RectPart::Right
  179. },
  180. };
  181. }
  182. std::vector<GroupMediaLayout> Layouter::layoutTwoLeftRightEqual() const {
  183. Expects(_count == 2);
  184. const auto width = (_maxWidth - _spacing) / 2;
  185. const auto height = Round(std::min(
  186. width / _ratios[0],
  187. std::min(width / _ratios[1], _maxHeight * 1.)));
  188. return {
  189. {
  190. QRect(0, 0, width, height),
  191. RectPart::Top | RectPart::Left | RectPart::Bottom
  192. },
  193. {
  194. QRect(width + _spacing, 0, width, height),
  195. RectPart::Top | RectPart::Right | RectPart::Bottom
  196. },
  197. };
  198. }
  199. std::vector<GroupMediaLayout> Layouter::layoutTwoLeftRight() const {
  200. Expects(_count == 2);
  201. const auto minimalWidth = Round(_minWidth * 1.5);
  202. const auto secondWidth = std::min(
  203. Round(std::max(
  204. 0.4 * (_maxWidth - _spacing),
  205. (_maxWidth - _spacing) / _ratios[0]
  206. / (1. / _ratios[0] + 1. / _ratios[1]))),
  207. _maxWidth - _spacing - minimalWidth);
  208. const auto firstWidth = _maxWidth
  209. - secondWidth
  210. - _spacing;
  211. const auto height = std::min(
  212. _maxHeight,
  213. Round(std::min(
  214. firstWidth / _ratios[0],
  215. secondWidth / _ratios[1])));
  216. return {
  217. {
  218. QRect(0, 0, firstWidth, height),
  219. RectPart::Top | RectPart::Left | RectPart::Bottom
  220. },
  221. {
  222. QRect(firstWidth + _spacing, 0, secondWidth, height),
  223. RectPart::Top | RectPart::Right | RectPart::Bottom
  224. },
  225. };
  226. }
  227. std::vector<GroupMediaLayout> Layouter::layoutThreeLeftAndOther() const {
  228. Expects(_count == 3);
  229. const auto firstHeight = _maxHeight;
  230. const auto thirdHeight = Round(std::min(
  231. (_maxHeight - _spacing) / 2.,
  232. (_ratios[1] * (_maxWidth - _spacing)
  233. / (_ratios[2] + _ratios[1]))));
  234. const auto secondHeight = firstHeight
  235. - thirdHeight
  236. - _spacing;
  237. const auto rightWidth = std::max(
  238. _minWidth,
  239. Round(std::min(
  240. (_maxWidth - _spacing) / 2.,
  241. std::min(
  242. thirdHeight * _ratios[2],
  243. secondHeight * _ratios[1]))));
  244. const auto leftWidth = std::min(
  245. Round(firstHeight * _ratios[0]),
  246. _maxWidth - _spacing - rightWidth);
  247. return {
  248. {
  249. QRect(0, 0, leftWidth, firstHeight),
  250. RectPart::Top | RectPart::Left | RectPart::Bottom
  251. },
  252. {
  253. QRect(leftWidth + _spacing, 0, rightWidth, secondHeight),
  254. RectPart::Top | RectPart::Right
  255. },
  256. {
  257. QRect(leftWidth + _spacing, secondHeight + _spacing, rightWidth, thirdHeight),
  258. RectPart::Bottom | RectPart::Right
  259. },
  260. };
  261. }
  262. std::vector<GroupMediaLayout> Layouter::layoutThreeTopAndOther() const {
  263. Expects(_count == 3);
  264. const auto firstWidth = _maxWidth;
  265. const auto firstHeight = Round(std::min(
  266. firstWidth / _ratios[0],
  267. (_maxHeight - _spacing) * 0.66));
  268. const auto secondWidth = (_maxWidth - _spacing) / 2;
  269. const auto secondHeight = std::min(
  270. _maxHeight - firstHeight - _spacing,
  271. Round(std::min(
  272. secondWidth / _ratios[1],
  273. secondWidth / _ratios[2])));
  274. const auto thirdWidth = firstWidth - secondWidth - _spacing;
  275. return {
  276. {
  277. QRect(0, 0, firstWidth, firstHeight),
  278. RectPart::Left | RectPart::Top | RectPart::Right
  279. },
  280. {
  281. QRect(0, firstHeight + _spacing, secondWidth, secondHeight),
  282. RectPart::Bottom | RectPart::Left
  283. },
  284. {
  285. QRect(secondWidth + _spacing, firstHeight + _spacing, thirdWidth, secondHeight),
  286. RectPart::Bottom | RectPart::Right
  287. },
  288. };
  289. }
  290. std::vector<GroupMediaLayout> Layouter::layoutFourTopAndOther() const {
  291. Expects(_count == 4);
  292. const auto w = _maxWidth;
  293. const auto h0 = Round(std::min(
  294. w / _ratios[0],
  295. (_maxHeight - _spacing) * 0.66));
  296. const auto h = Round(
  297. (_maxWidth - 2 * _spacing)
  298. / (_ratios[1] + _ratios[2] + _ratios[3]));
  299. const auto w0 = std::max(
  300. _minWidth,
  301. Round(std::min(
  302. (_maxWidth - 2 * _spacing) * 0.4,
  303. h * _ratios[1])));
  304. const auto w2 = Round(std::max(
  305. std::max(
  306. _minWidth * 1.,
  307. (_maxWidth - 2 * _spacing) * 0.33),
  308. h * _ratios[3]));
  309. const auto w1 = w - w0 - w2 - 2 * _spacing;
  310. const auto h1 = std::min(
  311. _maxHeight - h0 - _spacing,
  312. h);
  313. return {
  314. {
  315. QRect(0, 0, w, h0),
  316. RectPart::Left | RectPart::Top | RectPart::Right
  317. },
  318. {
  319. QRect(0, h0 + _spacing, w0, h1),
  320. RectPart::Bottom | RectPart::Left
  321. },
  322. {
  323. QRect(w0 + _spacing, h0 + _spacing, w1, h1),
  324. RectPart::Bottom,
  325. },
  326. {
  327. QRect(w0 + _spacing + w1 + _spacing, h0 + _spacing, w2, h1),
  328. RectPart::Right | RectPart::Bottom
  329. },
  330. };
  331. }
  332. std::vector<GroupMediaLayout> Layouter::layoutFourLeftAndOther() const {
  333. Expects(_count == 4);
  334. const auto h = _maxHeight;
  335. const auto w0 = Round(std::min(
  336. h * _ratios[0],
  337. (_maxWidth - _spacing) * 0.6));
  338. const auto w = Round(
  339. (_maxHeight - 2 * _spacing)
  340. / (1. / _ratios[1] + 1. / _ratios[2] + 1. / _ratios[3])
  341. );
  342. const auto h0 = Round(w / _ratios[1]);
  343. const auto h1 = Round(w / _ratios[2]);
  344. const auto h2 = h - h0 - h1 - 2 * _spacing;
  345. const auto w1 = std::max(
  346. _minWidth,
  347. std::min(_maxWidth - w0 - _spacing, w));
  348. return {
  349. {
  350. QRect(0, 0, w0, h),
  351. RectPart::Top | RectPart::Left | RectPart::Bottom
  352. },
  353. {
  354. QRect(w0 + _spacing, 0, w1, h0),
  355. RectPart::Top | RectPart::Right
  356. },
  357. {
  358. QRect(w0 + _spacing, h0 + _spacing, w1, h1),
  359. RectPart::Right
  360. },
  361. {
  362. QRect(w0 + _spacing, h0 + h1 + 2 * _spacing, w1, h2),
  363. RectPart::Bottom | RectPart::Right
  364. },
  365. };
  366. }
  367. ComplexLayouter::ComplexLayouter(
  368. const std::vector<float64> &ratios,
  369. float64 averageRatio,
  370. int maxWidth,
  371. int minWidth,
  372. int spacing)
  373. : _ratios(CropRatios(ratios, averageRatio))
  374. , _count(int(_ratios.size()))
  375. // All apps currently use square max size first.
  376. // In complex case they use maxWidth * 4 / 3 as maxHeight.
  377. , _maxWidth(maxWidth)
  378. , _maxHeight(maxWidth * 4 / 3)
  379. , _minWidth(minWidth)
  380. , _spacing(spacing)
  381. , _averageRatio(averageRatio) {
  382. }
  383. std::vector<float64> ComplexLayouter::CropRatios(
  384. const std::vector<float64> &ratios,
  385. float64 averageRatio) {
  386. return ranges::views::all(
  387. ratios
  388. ) | ranges::views::transform([&](float64 ratio) {
  389. constexpr auto kMaxRatio = 2.75;
  390. constexpr auto kMinRatio = 0.6667;
  391. return (averageRatio > 1.1)
  392. ? std::clamp(ratio, 1., kMaxRatio)
  393. : std::clamp(ratio, kMinRatio, 1.);
  394. }) | ranges::to_vector;
  395. }
  396. std::vector<GroupMediaLayout> ComplexLayouter::layout() const {
  397. Expects(_count > 1);
  398. auto result = std::vector<GroupMediaLayout>(_count);
  399. auto attempts = std::vector<Attempt>();
  400. const auto multiHeight = [&](int offset, int count) {
  401. const auto ratios = gsl::make_span(_ratios).subspan(offset, count);
  402. const auto sum = ranges::accumulate(ratios, 0.);
  403. return (_maxWidth - (count - 1) * _spacing) / sum;
  404. };
  405. const auto pushAttempt = [&](std::vector<int> lineCounts) {
  406. auto heights = std::vector<float64>();
  407. heights.reserve(lineCounts.size());
  408. auto offset = 0;
  409. for (auto count : lineCounts) {
  410. heights.push_back(multiHeight(offset, count));
  411. offset += count;
  412. }
  413. attempts.push_back({ std::move(lineCounts), std::move(heights) });
  414. };
  415. for (auto first = 1; first != _count; ++first) {
  416. const auto second = _count - first;
  417. if (first > 3 || second > 3) {
  418. continue;
  419. }
  420. pushAttempt({ first, second });
  421. }
  422. for (auto first = 1; first != _count - 1; ++first) {
  423. for (auto second = 1; second != _count - first; ++second) {
  424. const auto third = _count - first - second;
  425. if ((first > 3)
  426. || (second > ((_averageRatio < 0.85) ? 4 : 3))
  427. || (third > 3)) {
  428. continue;
  429. }
  430. pushAttempt({ first, second, third });
  431. }
  432. }
  433. for (auto first = 1; first != _count - 1; ++first) {
  434. for (auto second = 1; second != _count - first; ++second) {
  435. for (auto third = 1; third != _count - first - second; ++third) {
  436. const auto fourth = _count - first - second - third;
  437. if (first > 3 || second > 3 || third > 3 || fourth > 3) {
  438. continue;
  439. }
  440. pushAttempt({ first, second, third, fourth });
  441. }
  442. }
  443. }
  444. auto optimalAttempt = (const Attempt*)nullptr;
  445. auto optimalDiff = 0.;
  446. for (const auto &attempt : attempts) {
  447. const auto &heights = attempt.heights;
  448. const auto &counts = attempt.lineCounts;
  449. const auto lineCount = int(counts.size());
  450. const auto totalHeight = ranges::accumulate(heights, 0.)
  451. + _spacing * (lineCount - 1);
  452. const auto minLineHeight = ranges::min(heights);
  453. const auto bad1 = (minLineHeight < _minWidth) ? 1.5 : 1.;
  454. const auto bad2 = [&] {
  455. for (auto line = 1; line != lineCount; ++line) {
  456. if (counts[line - 1] > counts[line]) {
  457. return 1.5;
  458. }
  459. }
  460. return 1.;
  461. }();
  462. const auto diff = std::abs(totalHeight - _maxHeight) * bad1 * bad2;
  463. if (!optimalAttempt || diff < optimalDiff) {
  464. optimalAttempt = &attempt;
  465. optimalDiff = diff;
  466. }
  467. }
  468. Assert(optimalAttempt != nullptr);
  469. const auto &optimalCounts = optimalAttempt->lineCounts;
  470. const auto &optimalHeights = optimalAttempt->heights;
  471. const auto rowCount = int(optimalCounts.size());
  472. auto index = 0;
  473. auto y = 0.;
  474. for (auto row = 0; row != rowCount; ++row) {
  475. const auto colCount = optimalCounts[row];
  476. const auto lineHeight = optimalHeights[row];
  477. const auto height = Round(lineHeight);
  478. auto x = 0;
  479. for (auto col = 0; col != colCount; ++col) {
  480. const auto sides = RectPart::None
  481. | (row == 0 ? RectPart::Top : RectPart::None)
  482. | (row == rowCount - 1 ? RectPart::Bottom : RectPart::None)
  483. | (col == 0 ? RectPart::Left : RectPart::None)
  484. | (col == colCount - 1 ? RectPart::Right : RectPart::None);
  485. const auto ratio = _ratios[index];
  486. const auto width = (col == colCount - 1)
  487. ? (_maxWidth - x)
  488. : Round(ratio * lineHeight);
  489. result[index] = {
  490. QRect(x, y, width, height),
  491. sides
  492. };
  493. x += width + _spacing;
  494. ++index;
  495. }
  496. y += height + _spacing;
  497. }
  498. return result;
  499. }
  500. } // namespace
  501. std::vector<GroupMediaLayout> LayoutMediaGroup(
  502. const std::vector<QSize> &sizes,
  503. int maxWidth,
  504. int minWidth,
  505. int spacing) {
  506. return Layouter(sizes, maxWidth, minWidth, spacing).layout();
  507. }
  508. RectParts GetCornersFromSides(RectParts sides) {
  509. const auto convert = [&](
  510. RectPart side1,
  511. RectPart side2,
  512. RectPart corner) {
  513. return ((sides & side1) && (sides & side2))
  514. ? corner
  515. : RectPart::None;
  516. };
  517. return RectPart::None
  518. | convert(RectPart::Top, RectPart::Left, RectPart::TopLeft)
  519. | convert(RectPart::Top, RectPart::Right, RectPart::TopRight)
  520. | convert(RectPart::Bottom, RectPart::Left, RectPart::BottomLeft)
  521. | convert(RectPart::Bottom, RectPart::Right, RectPart::BottomRight);
  522. }
  523. QSize GetImageScaleSizeForGeometry(QSize original, QSize geometry) {
  524. const auto width = geometry.width();
  525. const auto height = geometry.height();
  526. auto tw = original.width();
  527. auto th = original.height();
  528. if (tw * height > th * width) {
  529. if (th > height || tw * height < 2 * th * width) {
  530. tw = (height * tw) / th;
  531. th = height;
  532. } else if (tw < width) {
  533. th = (width * th) / tw;
  534. tw = width;
  535. }
  536. } else {
  537. if (tw > width || th * width < 2 * tw * height) {
  538. th = (width * th) / tw;
  539. tw = width;
  540. } else if (tw > 0 && th < height) {
  541. tw = (height * tw) / th;
  542. th = height;
  543. }
  544. }
  545. if (tw < 1) tw = 1;
  546. if (th < 1) th = 1;
  547. return { tw, th };
  548. }
  549. } // namespace Ui