round_area_with_shadow.cpp 13 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471
  1. // This file is part of Desktop App Toolkit,
  2. // a set of libraries for developing nice desktop applications.
  3. //
  4. // For license and copyright information please follow this link:
  5. // https://github.com/desktop-app/legal/blob/master/LEGAL
  6. //
  7. #include "ui/effects/round_area_with_shadow.h"
  8. #include "ui/style/style_core.h"
  9. #include "ui/image/image_prepare.h"
  10. #include "ui/painter.h"
  11. namespace Ui {
  12. namespace {
  13. constexpr auto kBgCacheIndex = 0;
  14. constexpr auto kShadowCacheIndex = 0;
  15. constexpr auto kOverlayMaskCacheIndex = 0;
  16. constexpr auto kOverlayShadowCacheIndex = 1;
  17. constexpr auto kOverlayCacheColumsCount = 2;
  18. constexpr auto kDivider = 4;
  19. } // namespace
  20. [[nodiscard]] QImage RoundAreaWithShadow::PrepareImage(QSize size) {
  21. const auto ratio = style::DevicePixelRatio();
  22. auto result = QImage(
  23. size * ratio,
  24. QImage::Format_ARGB32_Premultiplied);
  25. result.setDevicePixelRatio(ratio);
  26. return result;
  27. }
  28. [[nodiscard]] QImage RoundAreaWithShadow::PrepareFramesCache(
  29. QSize frame,
  30. int columns) {
  31. static_assert(!(kFramesCount % kDivider));
  32. return PrepareImage(QSize(
  33. frame.width() * kDivider * columns,
  34. frame.height() * kFramesCount / kDivider));
  35. }
  36. [[nodiscard]] QRect RoundAreaWithShadow::FrameCacheRect(
  37. int frameIndex,
  38. int column,
  39. QSize frame) {
  40. const auto ratio = style::DevicePixelRatio();
  41. const auto origin = QPoint(
  42. frame.width() * (kDivider * column + (frameIndex % kDivider)),
  43. frame.height() * (frameIndex / kDivider));
  44. return QRect(ratio * origin, ratio * frame);
  45. }
  46. RoundAreaWithShadow::RoundAreaWithShadow(
  47. QSize inner,
  48. QMargins shadow,
  49. int twiceRadiusMax)
  50. : _inner({}, inner)
  51. , _outer(_inner.marginsAdded(shadow).size())
  52. , _overlay(QRect(
  53. 0,
  54. 0,
  55. std::max(inner.width(), twiceRadiusMax),
  56. std::max(inner.height(), twiceRadiusMax)).marginsAdded(shadow).size())
  57. , _cacheBg(PrepareFramesCache(_outer))
  58. , _shadowParts(PrepareFramesCache(_outer))
  59. , _overlayCacheParts(PrepareFramesCache(_overlay, kOverlayCacheColumsCount))
  60. , _overlayMaskScaled(PrepareImage(_overlay))
  61. , _overlayShadowScaled(PrepareImage(_overlay))
  62. , _shadowBuffer(PrepareImage(_outer)) {
  63. _inner.translate(QRect({}, _outer).center() - _inner.center());
  64. }
  65. ImageSubrect RoundAreaWithShadow::validateOverlayMask(
  66. int frameIndex,
  67. QSize innerSize,
  68. float64 radius,
  69. int twiceRadius,
  70. float64 scale) {
  71. const auto ratio = style::DevicePixelRatio();
  72. const auto cached = (scale == 1.);
  73. const auto full = cached
  74. ? FrameCacheRect(frameIndex, kOverlayMaskCacheIndex, _overlay)
  75. : QRect(QPoint(), _overlay * ratio);
  76. const auto minWidth = twiceRadius + _outer.width() - _inner.width();
  77. const auto minHeight = twiceRadius + _outer.height() - _inner.height();
  78. const auto maskSize = QSize(
  79. std::max(_outer.width(), minWidth),
  80. std::max(_outer.height(), minHeight));
  81. const auto result = ImageSubrect{
  82. cached ? &_overlayCacheParts : &_overlayMaskScaled,
  83. QRect(full.topLeft(), maskSize * ratio),
  84. };
  85. if (cached && _validOverlayMask[frameIndex]) {
  86. return result;
  87. }
  88. auto p = QPainter(result.image.get());
  89. const auto position = full.topLeft() / ratio;
  90. p.setCompositionMode(QPainter::CompositionMode_Source);
  91. p.fillRect(QRect(position, maskSize), Qt::transparent);
  92. p.setCompositionMode(QPainter::CompositionMode_SourceOver);
  93. auto hq = PainterHighQualityEnabler(p);
  94. const auto inner = QRect(position + _inner.topLeft(), innerSize);
  95. p.setPen(Qt::NoPen);
  96. p.setBrush(Qt::white);
  97. if (scale != 1.) {
  98. const auto center = inner.center();
  99. p.save();
  100. p.translate(center);
  101. p.scale(scale, scale);
  102. p.translate(-center);
  103. }
  104. p.drawRoundedRect(inner, radius, radius);
  105. if (scale != 1.) {
  106. p.restore();
  107. }
  108. if (cached) {
  109. _validOverlayMask[frameIndex] = true;
  110. }
  111. return result;
  112. }
  113. ImageSubrect RoundAreaWithShadow::validateOverlayShadow(
  114. int frameIndex,
  115. QSize innerSize,
  116. float64 radius,
  117. int twiceRadius,
  118. float64 scale,
  119. const ImageSubrect &mask) {
  120. const auto ratio = style::DevicePixelRatio();
  121. const auto cached = (scale == 1.);
  122. const auto full = cached
  123. ? FrameCacheRect(frameIndex, kOverlayShadowCacheIndex, _overlay)
  124. : QRect(QPoint(), _overlay * ratio);
  125. const auto minWidth = twiceRadius + _outer.width() - _inner.width();
  126. const auto minHeight = twiceRadius + _outer.height() - _inner.height();
  127. const auto maskSize = QSize(
  128. std::max(_outer.width(), minWidth),
  129. std::max(_outer.height(), minHeight));
  130. const auto result = ImageSubrect{
  131. cached ? &_overlayCacheParts : &_overlayShadowScaled,
  132. QRect(full.topLeft(), maskSize * ratio),
  133. };
  134. if (cached && _validOverlayShadow[frameIndex]) {
  135. return result;
  136. }
  137. const auto position = full.topLeft() / ratio;
  138. _overlayShadowScaled.fill(Qt::transparent);
  139. const auto inner = QRect(_inner.topLeft(), innerSize);
  140. const auto add = style::ConvertScale(2.5);
  141. const auto shift = style::ConvertScale(0.5);
  142. const auto extended = QRectF(inner).marginsAdded({ add, add, add, add });
  143. {
  144. auto p = QPainter(&_overlayShadowScaled);
  145. p.setCompositionMode(QPainter::CompositionMode_Source);
  146. auto hq = PainterHighQualityEnabler(p);
  147. p.setPen(Qt::NoPen);
  148. p.setBrush(_shadow);
  149. if (scale != 1.) {
  150. const auto center = inner.center();
  151. p.translate(center);
  152. p.scale(scale, scale);
  153. p.translate(-center);
  154. }
  155. p.drawRoundedRect(extended.translated(0, shift), radius, radius);
  156. p.end();
  157. }
  158. _overlayShadowScaled = Images::Blur(std::move(_overlayShadowScaled));
  159. auto q = Painter(result.image);
  160. if (result.image != &_overlayShadowScaled) {
  161. q.setCompositionMode(QPainter::CompositionMode_Source);
  162. q.drawImage(
  163. QRect(position, maskSize),
  164. _overlayShadowScaled,
  165. QRect(QPoint(), maskSize * ratio));
  166. }
  167. q.setCompositionMode(QPainter::CompositionMode_DestinationOut);
  168. q.drawImage(QRect(position, maskSize), *mask.image, mask.rect);
  169. if (cached) {
  170. _validOverlayShadow[frameIndex] = true;
  171. }
  172. return result;
  173. }
  174. void RoundAreaWithShadow::overlayExpandedBorder(
  175. QPainter &p,
  176. QSize size,
  177. float64 expandRatio,
  178. float64 radiusFrom,
  179. float64 radiusTill,
  180. float64 scale) {
  181. const auto progress = expandRatio;
  182. const auto frame = int(base::SafeRound(progress * (kFramesCount - 1)));
  183. const auto cacheRatio = frame / float64(kFramesCount - 1);
  184. const auto radius = radiusFrom + (radiusTill - radiusFrom) * cacheRatio;
  185. const auto twiceRadius = int(base::SafeRound(radius * 2));
  186. const auto innerSize = QSize(
  187. std::max(_inner.width(), twiceRadius),
  188. std::max(_inner.height(), twiceRadius));
  189. const auto overlayMask = validateOverlayMask(
  190. frame,
  191. innerSize,
  192. radius,
  193. twiceRadius,
  194. scale);
  195. const auto overlayShadow = validateOverlayShadow(
  196. frame,
  197. innerSize,
  198. radius,
  199. twiceRadius,
  200. scale,
  201. overlayMask);
  202. p.setCompositionMode(QPainter::CompositionMode_DestinationIn);
  203. FillWithImage(p, QRect(QPoint(), size), overlayMask);
  204. p.setCompositionMode(QPainter::CompositionMode_SourceOver);
  205. FillWithImage(p, QRect(QPoint(), size), overlayShadow);
  206. }
  207. QRect RoundAreaWithShadow::FillWithImage(
  208. QPainter &p,
  209. QRect geometry,
  210. const ImageSubrect &pattern) {
  211. const auto factor = style::DevicePixelRatio();
  212. const auto &image = *pattern.image;
  213. const auto source = pattern.rect;
  214. const auto sourceWidth = (source.width() / factor);
  215. const auto sourceHeight = (source.height() / factor);
  216. if (geometry.width() == sourceWidth) {
  217. const auto part = (sourceHeight / 2) - 1;
  218. const auto fill = geometry.height() - 2 * part;
  219. const auto half = part * factor;
  220. const auto top = source.height() - half;
  221. p.drawImage(
  222. geometry.topLeft(),
  223. image,
  224. QRect(source.x(), source.y(), source.width(), half));
  225. if (fill > 0) {
  226. p.drawImage(
  227. QRect(
  228. geometry.topLeft() + QPoint(0, part),
  229. QSize(sourceWidth, fill)),
  230. image,
  231. QRect(
  232. source.x(),
  233. source.y() + half,
  234. source.width(),
  235. top - half));
  236. }
  237. p.drawImage(
  238. geometry.topLeft() + QPoint(0, part + fill),
  239. image,
  240. QRect(source.x(), source.y() + top, source.width(), half));
  241. return QRect();
  242. } else if (geometry.height() == sourceHeight) {
  243. const auto part = (sourceWidth / 2) - 1;
  244. const auto fill = geometry.width() - 2 * part;
  245. const auto half = part * factor;
  246. const auto left = source.width() - half;
  247. p.drawImage(
  248. geometry.topLeft(),
  249. image,
  250. QRect(source.x(), source.y(), half, source.height()));
  251. if (fill > 0) {
  252. p.drawImage(
  253. QRect(
  254. geometry.topLeft() + QPoint(part, 0),
  255. QSize(fill, sourceHeight)),
  256. image,
  257. QRect(
  258. source.x() + half,
  259. source.y(),
  260. left - half,
  261. source.height()));
  262. }
  263. p.drawImage(
  264. geometry.topLeft() + QPoint(part + fill, 0),
  265. image,
  266. QRect(source.x() + left, source.y(), half, source.height()));
  267. return QRect();
  268. } else if (geometry.width() > sourceWidth
  269. && geometry.height() > sourceHeight) {
  270. const auto xpart = (sourceWidth / 2) - 1;
  271. const auto xfill = geometry.width() - 2 * xpart;
  272. const auto xhalf = xpart * factor;
  273. const auto left = source.width() - xhalf;
  274. const auto ypart = (sourceHeight / 2) - 1;
  275. const auto yfill = geometry.height() - 2 * ypart;
  276. const auto yhalf = ypart * factor;
  277. const auto top = source.height() - yhalf;
  278. p.drawImage(
  279. geometry.topLeft(),
  280. image,
  281. QRect(source.x(), source.y(), xhalf, yhalf));
  282. if (xfill > 0) {
  283. p.drawImage(
  284. QRect(
  285. geometry.topLeft() + QPoint(xpart, 0),
  286. QSize(xfill, ypart)),
  287. image,
  288. QRect(
  289. source.x() + xhalf,
  290. source.y(),
  291. left - xhalf,
  292. yhalf));
  293. }
  294. p.drawImage(
  295. geometry.topLeft() + QPoint(xpart + xfill, 0),
  296. image,
  297. QRect(source.x() + left, source.y(), xhalf, yhalf));
  298. if (yfill > 0) {
  299. p.drawImage(
  300. QRect(
  301. geometry.topLeft() + QPoint(0, ypart),
  302. QSize(xpart, yfill)),
  303. image,
  304. QRect(
  305. source.x(),
  306. source.y() + yhalf,
  307. xhalf,
  308. top - yhalf));
  309. p.drawImage(
  310. QRect(
  311. geometry.topLeft() + QPoint(xpart + xfill, ypart),
  312. QSize(xpart, yfill)),
  313. image,
  314. QRect(
  315. source.x() + left,
  316. source.y() + yhalf,
  317. xhalf,
  318. top - yhalf));
  319. }
  320. p.drawImage(
  321. geometry.topLeft() + QPoint(0, ypart + yfill),
  322. image,
  323. QRect(source.x(), source.y() + top, xhalf, yhalf));
  324. if (xfill > 0) {
  325. p.drawImage(
  326. QRect(
  327. geometry.topLeft() + QPoint(xpart, ypart + yfill),
  328. QSize(xfill, ypart)),
  329. image,
  330. QRect(
  331. source.x() + xhalf,
  332. source.y() + top,
  333. left - xhalf,
  334. yhalf));
  335. }
  336. p.drawImage(
  337. geometry.topLeft() + QPoint(xpart + xfill, ypart + yfill),
  338. image,
  339. QRect(source.x() + left, source.y() + top, xhalf, yhalf));
  340. return QRect(
  341. geometry.topLeft() + QPoint(xpart, ypart),
  342. QSize(xfill, yfill));
  343. } else {
  344. Unexpected("Values in RoundAreaWithShadow::fillWithImage.");
  345. }
  346. }
  347. void RoundAreaWithShadow::setShadowColor(const QColor &shadow) {
  348. if (_shadow == shadow) {
  349. return;
  350. }
  351. _shadow = shadow;
  352. ranges::fill(_validBg, false);
  353. ranges::fill(_validShadow, false);
  354. ranges::fill(_validOverlayShadow, false);
  355. }
  356. QRect RoundAreaWithShadow::validateShadow(
  357. int frameIndex,
  358. float64 scale,
  359. float64 radius) {
  360. const auto rect = FrameCacheRect(frameIndex, kShadowCacheIndex, _outer);
  361. if (_validShadow[frameIndex]) {
  362. return rect;
  363. }
  364. _shadowBuffer.fill(Qt::transparent);
  365. auto p = QPainter(&_shadowBuffer);
  366. auto hq = PainterHighQualityEnabler(p);
  367. const auto center = _inner.center();
  368. const auto add = style::ConvertScale(2.5);
  369. const auto shift = style::ConvertScale(0.5);
  370. const auto big = QRectF(_inner).marginsAdded({ add, add, add, add });
  371. p.setPen(Qt::NoPen);
  372. p.setBrush(_shadow);
  373. if (scale != 1.) {
  374. p.translate(center);
  375. p.scale(scale, scale);
  376. p.translate(-center);
  377. }
  378. p.drawRoundedRect(big.translated(0, shift), radius, radius);
  379. p.end();
  380. _shadowBuffer = Images::Blur(std::move(_shadowBuffer));
  381. auto q = QPainter(&_shadowParts);
  382. q.setCompositionMode(QPainter::CompositionMode_Source);
  383. q.drawImage(rect.topLeft() / style::DevicePixelRatio(), _shadowBuffer);
  384. _validShadow[frameIndex] = true;
  385. return rect;
  386. }
  387. void RoundAreaWithShadow::setBackgroundColor(const QColor &background) {
  388. if (_background == background) {
  389. return;
  390. }
  391. _background = background;
  392. ranges::fill(_validBg, false);
  393. }
  394. ImageSubrect RoundAreaWithShadow::validateFrame(
  395. int frameIndex,
  396. float64 scale,
  397. float64 radius) {
  398. const auto result = ImageSubrect{
  399. &_cacheBg,
  400. FrameCacheRect(frameIndex, kBgCacheIndex, _outer)
  401. };
  402. if (_validBg[frameIndex]) {
  403. return result;
  404. }
  405. const auto position = result.rect.topLeft() / style::DevicePixelRatio();
  406. const auto inner = _inner.translated(position);
  407. const auto shadowSource = validateShadow(frameIndex, scale, radius);
  408. auto p = QPainter(&_cacheBg);
  409. p.setCompositionMode(QPainter::CompositionMode_Source);
  410. p.drawImage(position, _shadowParts, shadowSource);
  411. p.setCompositionMode(QPainter::CompositionMode_SourceOver);
  412. auto hq = PainterHighQualityEnabler(p);
  413. p.setPen(Qt::NoPen);
  414. p.setBrush(_background);
  415. if (scale != 1.) {
  416. const auto center = inner.center();
  417. p.save();
  418. p.translate(center);
  419. p.scale(scale, scale);
  420. p.translate(-center);
  421. }
  422. p.drawRoundedRect(inner, radius, radius);
  423. if (scale != 1.) {
  424. p.restore();
  425. }
  426. _validBg[frameIndex] = true;
  427. return result;
  428. }
  429. } // namespace Ui