1 /*
2 This file is part of Telegram Desktop,
3 the official desktop application for the Telegram messaging service.
4 
5 For license and copyright information please follow this link:
6 https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
7 */
8 #include "calls/group/calls_group_viewport.h"
9 
10 #include "calls/group/calls_group_viewport_tile.h"
11 #include "calls/group/calls_group_viewport_opengl.h"
12 #include "calls/group/calls_group_viewport_raster.h"
13 #include "calls/group/calls_group_common.h"
14 #include "calls/group/calls_group_call.h"
15 #include "calls/group/calls_group_members_row.h"
16 #include "media/view/media_view_pip.h"
17 #include "base/platform/base_platform_info.h"
18 #include "webrtc/webrtc_video_track.h"
19 #include "ui/painter.h"
20 #include "ui/abstract_button.h"
21 #include "ui/gl/gl_surface.h"
22 #include "ui/effects/animations.h"
23 #include "ui/effects/cross_line.h"
24 #include "data/data_group_call.h" // MuteButtonTooltip.
25 #include "lang/lang_keys.h"
26 #include "styles/style_calls.h"
27 
28 #include <QtGui/QtEvents>
29 #include <QOpenGLShader>
30 
31 namespace Calls::Group {
32 namespace {
33 
InterpolateRect(QRect a,QRect b,float64 ratio)34 [[nodiscard]] QRect InterpolateRect(QRect a, QRect b, float64 ratio) {
35 	const auto left = anim::interpolate(a.x(), b.x(), ratio);
36 	const auto top = anim::interpolate(a.y(), b.y(), ratio);
37 	const auto right = anim::interpolate(
38 		a.x() + a.width(),
39 		b.x() + b.width(),
40 		ratio);
41 	const auto bottom = anim::interpolate(
42 		a.y() + a.height(),
43 		b.y() + b.height(),
44 		ratio);
45 	return { left, top, right - left, bottom - top };
46 }
47 
48 } // namespace
49 
Viewport(not_null<QWidget * > parent,PanelMode mode,Ui::GL::Backend backend)50 Viewport::Viewport(
51 	not_null<QWidget*> parent,
52 	PanelMode mode,
53 	Ui::GL::Backend backend)
54 : _mode(mode)
55 , _content(Ui::GL::CreateSurface(parent, chooseRenderer(backend))) {
56 	setup();
57 }
58 
59 Viewport::~Viewport() = default;
60 
widget() const61 not_null<QWidget*> Viewport::widget() const {
62 	return _content->rpWidget();
63 }
64 
rp() const65 not_null<Ui::RpWidgetWrap*> Viewport::rp() const {
66 	return _content.get();
67 }
68 
setup()69 void Viewport::setup() {
70 	const auto raw = widget();
71 
72 	raw->resize(0, 0);
73 	raw->setAttribute(Qt::WA_OpaquePaintEvent);
74 	raw->setMouseTracking(true);
75 
76 	_content->sizeValue(
77 	) | rpl::filter([=] {
78 		return wide();
79 	}) | rpl::start_with_next([=] {
80 		updateTilesGeometry();
81 	}, lifetime());
82 
83 	_content->events(
84 	) | rpl::start_with_next([=](not_null<QEvent*> e) {
85 		const auto type = e->type();
86 		if (type == QEvent::Enter) {
87 			Ui::Integration::Instance().registerLeaveSubscription(raw);
88 			_mouseInside = true;
89 		} else if (type == QEvent::Leave) {
90 			Ui::Integration::Instance().unregisterLeaveSubscription(raw);
91 			setSelected({});
92 			_mouseInside = false;
93 		} else if (type == QEvent::MouseButtonPress) {
94 			handleMousePress(
95 				static_cast<QMouseEvent*>(e.get())->pos(),
96 				static_cast<QMouseEvent*>(e.get())->button());
97 		} else if (type == QEvent::MouseButtonRelease) {
98 			handleMouseRelease(
99 				static_cast<QMouseEvent*>(e.get())->pos(),
100 				static_cast<QMouseEvent*>(e.get())->button());
101 		} else if (type == QEvent::MouseMove) {
102 			handleMouseMove(static_cast<QMouseEvent*>(e.get())->pos());
103 		}
104 	}, lifetime());
105 }
106 
setGeometry(QRect geometry)107 void Viewport::setGeometry(QRect geometry) {
108 	Expects(wide());
109 
110 	if (widget()->geometry() != geometry) {
111 		_geometryStaleAfterModeChange = false;
112 		widget()->setGeometry(geometry);
113 	} else if (_geometryStaleAfterModeChange) {
114 		_geometryStaleAfterModeChange = false;
115 		updateTilesGeometry();
116 	}
117 }
118 
resizeToWidth(int width)119 void Viewport::resizeToWidth(int width) {
120 	Expects(!wide());
121 
122 	updateTilesGeometry(width);
123 }
124 
setScrollTop(int scrollTop)125 void Viewport::setScrollTop(int scrollTop) {
126 	if (_scrollTop == scrollTop) {
127 		return;
128 	}
129 	_scrollTop = scrollTop;
130 	updateTilesGeometry();
131 }
132 
wide() const133 bool Viewport::wide() const {
134 	return (_mode == PanelMode::Wide);
135 }
136 
setMode(PanelMode mode,not_null<QWidget * > parent)137 void Viewport::setMode(PanelMode mode, not_null<QWidget*> parent) {
138 	if (_mode == mode && widget()->parent() == parent) {
139 		return;
140 	}
141 	_mode = mode;
142 	_scrollTop = 0;
143 	setControlsShown(1.);
144 	if (widget()->parent() != parent) {
145 		const auto hidden = widget()->isHidden();
146 		widget()->setParent(parent);
147 		if (!hidden) {
148 			widget()->show();
149 		}
150 	}
151 	if (!wide()) {
152 		for (const auto &tile : _tiles) {
153 			tile->toggleTopControlsShown(false);
154 		}
155 	} else if (_selected.tile) {
156 		_selected.tile->toggleTopControlsShown(true);
157 	}
158 }
159 
handleMousePress(QPoint position,Qt::MouseButton button)160 void Viewport::handleMousePress(QPoint position, Qt::MouseButton button) {
161 	handleMouseMove(position);
162 	setPressed(_selected);
163 }
164 
handleMouseRelease(QPoint position,Qt::MouseButton button)165 void Viewport::handleMouseRelease(QPoint position, Qt::MouseButton button) {
166 	handleMouseMove(position);
167 	const auto pressed = _pressed;
168 	setPressed({});
169 	if (const auto tile = pressed.tile) {
170 		if (pressed == _selected) {
171 			if (button == Qt::RightButton) {
172 				tile->row()->showContextMenu();
173 			} else if (!wide()
174 				|| (_hasTwoOrMore && !_large)
175 				|| pressed.element != Selection::Element::PinButton) {
176 				_clicks.fire_copy(tile->endpoint());
177 			} else if (pressed.element == Selection::Element::PinButton) {
178 				_pinToggles.fire(!tile->pinned());
179 			}
180 		}
181 	}
182 }
183 
handleMouseMove(QPoint position)184 void Viewport::handleMouseMove(QPoint position) {
185 	updateSelected(position);
186 }
187 
updateSelected(QPoint position)188 void Viewport::updateSelected(QPoint position) {
189 	if (!widget()->rect().contains(position)) {
190 		setSelected({});
191 		return;
192 	}
193 	for (const auto &tile : _tiles) {
194 		const auto geometry = tile->visible()
195 			? tile->geometry()
196 			: QRect();
197 		if (geometry.contains(position)) {
198 			const auto pin = wide()
199 				&& tile->pinOuter().contains(position - geometry.topLeft());
200 			const auto back = wide()
201 				&& tile->backOuter().contains(position - geometry.topLeft());
202 			setSelected({
203 				.tile = tile.get(),
204 				.element = (pin
205 					? Selection::Element::PinButton
206 					: back
207 					? Selection::Element::BackButton
208 					: Selection::Element::Tile),
209 			});
210 			return;
211 		}
212 	}
213 	setSelected({});
214 }
215 
updateSelected()216 void Viewport::updateSelected() {
217 	updateSelected(widget()->mapFromGlobal(QCursor::pos()));
218 }
219 
setControlsShown(float64 shown)220 void Viewport::setControlsShown(float64 shown) {
221 	_controlsShownRatio = shown;
222 	widget()->update();
223 }
224 
add(const VideoEndpoint & endpoint,VideoTileTrack track,rpl::producer<QSize> trackSize,rpl::producer<bool> pinned)225 void Viewport::add(
226 		const VideoEndpoint &endpoint,
227 		VideoTileTrack track,
228 		rpl::producer<QSize> trackSize,
229 		rpl::producer<bool> pinned) {
230 	_tiles.push_back(std::make_unique<VideoTile>(
231 		endpoint,
232 		track,
233 		std::move(trackSize),
234 		std::move(pinned),
235 		[=] { widget()->update(); }));
236 
237 	_tiles.back()->trackSizeValue(
238 	) | rpl::filter([](QSize size) {
239 		return !size.isEmpty();
240 	}) | rpl::start_with_next([=] {
241 		updateTilesGeometry();
242 	}, _tiles.back()->lifetime());
243 
244 	_tiles.back()->track()->stateValue(
245 	) | rpl::start_with_next([=] {
246 		updateTilesGeometry();
247 	}, _tiles.back()->lifetime());
248 }
249 
remove(const VideoEndpoint & endpoint)250 void Viewport::remove(const VideoEndpoint &endpoint) {
251 	const auto i = ranges::find(_tiles, endpoint, &VideoTile::endpoint);
252 	if (i == end(_tiles)) {
253 		return;
254 	}
255 	const auto removing = i->get();
256 	const auto largeRemoved = (_large == removing);
257 	if (largeRemoved) {
258 		prepareLargeChangeAnimation();
259 		_large = nullptr;
260 	}
261 	if (_selected.tile == removing) {
262 		setSelected({});
263 	}
264 	if (_pressed.tile == removing) {
265 		setPressed({});
266 	}
267 	for (auto &geometry : _startTilesLayout.list) {
268 		if (geometry.tile == removing) {
269 			geometry.tile = nullptr;
270 		}
271 	}
272 	for (auto &geometry : _finishTilesLayout.list) {
273 		if (geometry.tile == removing) {
274 			geometry.tile = nullptr;
275 		}
276 	}
277 	_tiles.erase(i);
278 	if (largeRemoved) {
279 		startLargeChangeAnimation();
280 	} else {
281 		updateTilesGeometry();
282 	}
283 }
284 
prepareLargeChangeAnimation()285 void Viewport::prepareLargeChangeAnimation() {
286 	if (!wide()) {
287 		return;
288 	} else if (_largeChangeAnimation.animating()) {
289 		updateTilesAnimated();
290 		const auto field = _finishTilesLayout.useColumns
291 			? &Geometry::columns
292 			: &Geometry::rows;
293 		for (auto &finish : _finishTilesLayout.list) {
294 			const auto tile = finish.tile;
295 			if (!tile) {
296 				continue;
297 			}
298 			finish.*field = tile->geometry();
299 		}
300 		_startTilesLayout = std::move(_finishTilesLayout);
301 		_largeChangeAnimation.stop();
302 
303 		_startTilesLayout.list.erase(
304 			ranges::remove(_startTilesLayout.list, nullptr, &Geometry::tile),
305 			end(_startTilesLayout.list));
306 	} else {
307 		_startTilesLayout = applyLarge(std::move(_startTilesLayout));
308 	}
309 }
310 
startLargeChangeAnimation()311 void Viewport::startLargeChangeAnimation() {
312 	Expects(!_largeChangeAnimation.animating());
313 
314 	if (!wide()
315 		|| anim::Disabled()
316 		|| (_startTilesLayout.list.size() < 2)
317 		|| !_opengl
318 		|| widget()->size().isEmpty()) {
319 		updateTilesGeometry();
320 		return;
321 	}
322 	_finishTilesLayout = applyLarge(
323 		countWide(widget()->width(), widget()->height()));
324 	if (_finishTilesLayout.list.empty()
325 		|| _finishTilesLayout.outer != _startTilesLayout.outer) {
326 		updateTilesGeometry();
327 		return;
328 	}
329 	_largeChangeAnimation.start(
330 		[=] { updateTilesAnimated(); },
331 		0.,
332 		1.,
333 		st::slideDuration);
334 }
335 
applyLarge(Layout layout) const336 Viewport::Layout Viewport::applyLarge(Layout layout) const {
337 	auto &list = layout.list;
338 	if (!_large) {
339 		return layout;
340 	}
341 	const auto i = ranges::find(list, _large, &Geometry::tile);
342 	if (i == end(list)) {
343 		return layout;
344 	}
345 	const auto field = layout.useColumns
346 		? &Geometry::columns
347 		: &Geometry::rows;
348 	const auto fullWidth = layout.outer.width();
349 	const auto fullHeight = layout.outer.height();
350 	const auto largeRect = (*i).*field;
351 	const auto largeLeft = largeRect.x();
352 	const auto largeTop = largeRect.y();
353 	const auto largeRight = largeLeft + largeRect.width();
354 	const auto largeBottom = largeTop + largeRect.height();
355 	for (auto &geometry : list) {
356 		if (geometry.tile == _large) {
357 			geometry.*field = { QPoint(), layout.outer };
358 		} else if (layout.useColumns) {
359 			auto &rect = geometry.columns;
360 			const auto center = rect.center();
361 			if (center.x() < largeLeft) {
362 				rect = rect.translated(-largeLeft, 0);
363 			} else if (center.x() > largeRight) {
364 				rect = rect.translated(fullWidth - largeRight, 0);
365 			} else if (center.y() < largeTop) {
366 				rect = QRect(
367 					0,
368 					rect.y() - largeTop,
369 					fullWidth,
370 					rect.height());
371 			} else if (center.y() > largeBottom) {
372 				rect = QRect(
373 					0,
374 					rect.y() + (fullHeight - largeBottom),
375 					fullWidth,
376 					rect.height());
377 			}
378 		} else {
379 			auto &rect = geometry.rows;
380 			const auto center = rect.center();
381 			if (center.y() < largeTop) {
382 				rect = rect.translated(0, -largeTop);
383 			} else if (center.y() > largeBottom) {
384 				rect = rect.translated(0, fullHeight - largeBottom);
385 			} else if (center.x() < largeLeft) {
386 				rect = QRect(
387 					rect.x() - largeLeft,
388 					0,
389 					rect.width(),
390 					fullHeight);
391 			} else {
392 				rect = QRect(
393 					rect.x() + (fullWidth - largeRight),
394 					0,
395 					rect.width(),
396 					fullHeight);
397 			}
398 		}
399 	}
400 	return layout;
401 }
402 
updateTilesAnimated()403 void Viewport::updateTilesAnimated() {
404 	if (!_largeChangeAnimation.animating()) {
405 		updateTilesGeometry();
406 		return;
407 	}
408 	const auto ratio = _largeChangeAnimation.value(1.);
409 	const auto field = _finishTilesLayout.useColumns
410 		? &Geometry::columns
411 		: &Geometry::rows;
412 	for (const auto &finish : _finishTilesLayout.list) {
413 		const auto tile = finish.tile;
414 		if (!tile) {
415 			continue;
416 		}
417 		const auto i = ranges::find(
418 			_startTilesLayout.list,
419 			tile,
420 			&Geometry::tile);
421 		if (i == end(_startTilesLayout.list)) {
422 			LOG(("Tiles Animation Error 1!"));
423 			_largeChangeAnimation.stop();
424 			updateTilesGeometry();
425 			return;
426 		}
427 		const auto from = (*i).*field;
428 		const auto to = finish.*field;
429 		tile->setGeometry(
430 			InterpolateRect(from, to, ratio),
431 			TileAnimation{ from.size(), to.size(), ratio });
432 	}
433 	widget()->update();
434 }
435 
countWide(int outerWidth,int outerHeight) const436 Viewport::Layout Viewport::countWide(int outerWidth, int outerHeight) const {
437 	auto result = Layout{ .outer = QSize(outerWidth, outerHeight) };
438 	auto &sizes = result.list;
439 	sizes.reserve(_tiles.size());
440 	for (const auto &tile : _tiles) {
441 		const auto video = tile.get();
442 		const auto size = video->trackOrUserpicSize();
443 		if (!size.isEmpty()) {
444 			sizes.push_back(Geometry{ video, size });
445 		}
446 	}
447 	if (sizes.empty()) {
448 		return result;
449 	} else if (sizes.size() == 1) {
450 		sizes.front().rows = { 0, 0, outerWidth, outerHeight };
451 		return result;
452 	}
453 
454 	auto columnsBlack = uint64();
455 	auto rowsBlack = uint64();
456 	const auto count = int(sizes.size());
457 	const auto skip = st::groupCallVideoLargeSkip;
458 	const auto slices = int(std::ceil(std::sqrt(float64(count))));
459 	{
460 		auto index = 0;
461 		const auto columns = slices;
462 		const auto sizew = (outerWidth + skip) / float64(columns);
463 		for (auto column = 0; column != columns; ++column) {
464 			const auto left = int(base::SafeRound(column * sizew));
465 			const auto width = int(
466 				base::SafeRound(column * sizew + sizew - skip)) - left;
467 			const auto rows = int(base::SafeRound((count - index)
468 				/ float64(columns - column)));
469 			const auto sizeh = (outerHeight + skip) / float64(rows);
470 			for (auto row = 0; row != rows; ++row) {
471 				const auto top = int(base::SafeRound(row * sizeh));
472 				const auto height = int(base::SafeRound(
473 					row * sizeh + sizeh - skip)) - top;
474 				auto &geometry = sizes[index];
475 				geometry.columns = {
476 					left,
477 					top,
478 					width,
479 					height };
480 				const auto scaled = geometry.size.scaled(
481 					width,
482 					height,
483 					Qt::KeepAspectRatio);
484 				columnsBlack += (scaled.width() < width)
485 					? (width - scaled.width()) * height
486 					: (height - scaled.height()) * width;
487 				++index;
488 			}
489 		}
490 	}
491 	{
492 		auto index = 0;
493 		const auto rows = slices;
494 		const auto sizeh = (outerHeight + skip) / float64(rows);
495 		for (auto row = 0; row != rows; ++row) {
496 			const auto top = int(base::SafeRound(row * sizeh));
497 			const auto height = int(
498 				base::SafeRound(row * sizeh + sizeh - skip)) - top;
499 			const auto columns = int(base::SafeRound((count - index)
500 				/ float64(rows - row)));
501 			const auto sizew = (outerWidth + skip) / float64(columns);
502 			for (auto column = 0; column != columns; ++column) {
503 				const auto left = int(base::SafeRound(column * sizew));
504 				const auto width = int(base::SafeRound(
505 					column * sizew + sizew - skip)) - left;
506 				auto &geometry = sizes[index];
507 				geometry.rows = {
508 					left,
509 					top,
510 					width,
511 					height };
512 				const auto scaled = geometry.size.scaled(
513 					width,
514 					height,
515 					Qt::KeepAspectRatio);
516 				rowsBlack += (scaled.width() < width)
517 					? (width - scaled.width()) * height
518 					: (height - scaled.height()) * width;
519 				++index;
520 			}
521 		}
522 	}
523 	result.useColumns = (columnsBlack < rowsBlack);
524 	return result;
525 }
526 
showLarge(const VideoEndpoint & endpoint)527 void Viewport::showLarge(const VideoEndpoint &endpoint) {
528 	// If a video get's switched off, GroupCall first unpins it,
529 	// then removes it from Large endpoint, then removes from active tracks.
530 	//
531 	// If we want to animate large video removal properly, we need to
532 	// delay this update and start animation directly from removing of the
533 	// track from the active list. Otherwise final state won't be correct.
534 	_updateLargeScheduled = [=] {
535 		const auto i = ranges::find(_tiles, endpoint, &VideoTile::endpoint);
536 		const auto large = (i != end(_tiles)) ? i->get() : nullptr;
537 		if (_large != large) {
538 			prepareLargeChangeAnimation();
539 			_large = large;
540 			updateTopControlsVisibility();
541 			startLargeChangeAnimation();
542 		}
543 
544 		Ensures(!_large || !_large->trackOrUserpicSize().isEmpty());
545 	};
546 	crl::on_main(widget(), [=] {
547 		if (!_updateLargeScheduled) {
548 			return;
549 		}
550 		base::take(_updateLargeScheduled)();
551 	});
552 }
553 
updateTilesGeometry()554 void Viewport::updateTilesGeometry() {
555 	updateTilesGeometry(widget()->width());
556 }
557 
updateTilesGeometry(int outerWidth)558 void Viewport::updateTilesGeometry(int outerWidth) {
559 	const auto mouseInside = _mouseInside.current();
560 	const auto guard = gsl::finally([&] {
561 		if (mouseInside) {
562 			updateSelected();
563 		}
564 		widget()->update();
565 	});
566 
567 	const auto outerHeight = widget()->height();
568 	if (_tiles.empty() || !outerWidth) {
569 		_fullHeight = 0;
570 		return;
571 	}
572 
573 	if (wide()) {
574 		updateTilesGeometryWide(outerWidth, outerHeight);
575 		refreshHasTwoOrMore();
576 		_fullHeight = 0;
577 	} else {
578 		updateTilesGeometryNarrow(outerWidth);
579 	}
580 }
581 
refreshHasTwoOrMore()582 void Viewport::refreshHasTwoOrMore() {
583 	auto hasTwoOrMore = false;
584 	auto oneFound = false;
585 	for (const auto &tile : _tiles) {
586 		if (!tile->trackOrUserpicSize().isEmpty()) {
587 			if (oneFound) {
588 				hasTwoOrMore = true;
589 				break;
590 			}
591 			oneFound = true;
592 		}
593 	}
594 	if (_hasTwoOrMore == hasTwoOrMore) {
595 		return;
596 	}
597 	_hasTwoOrMore = hasTwoOrMore;
598 	updateCursor();
599 	updateTopControlsVisibility();
600 }
601 
updateTopControlsVisibility()602 void Viewport::updateTopControlsVisibility() {
603 	if (_selected.tile) {
604 		_selected.tile->toggleTopControlsShown(
605 			_hasTwoOrMore && wide() && _large && _large == _selected.tile);
606 	}
607 }
608 
updateTilesGeometryWide(int outerWidth,int outerHeight)609 void Viewport::updateTilesGeometryWide(int outerWidth, int outerHeight) {
610 	if (!outerHeight) {
611 		return;
612 	} else if (_largeChangeAnimation.animating()) {
613 		if (_startTilesLayout.outer == QSize(outerWidth, outerHeight)) {
614 			return;
615 		}
616 		_largeChangeAnimation.stop();
617 	}
618 
619 	_startTilesLayout = countWide(outerWidth, outerHeight);
620 	if (_large && !_large->trackOrUserpicSize().isEmpty()) {
621 		for (const auto &geometry : _startTilesLayout.list) {
622 			if (geometry.tile == _large) {
623 				setTileGeometry(_large, { 0, 0, outerWidth, outerHeight });
624 			} else {
625 				geometry.tile->hide();
626 			}
627 		}
628 	} else {
629 		const auto field = _startTilesLayout.useColumns
630 			? &Geometry::columns
631 			: &Geometry::rows;
632 		for (const auto &geometry : _startTilesLayout.list) {
633 			if (const auto video = geometry.tile) {
634 				setTileGeometry(video, geometry.*field);
635 			}
636 		}
637 	}
638 }
639 
updateTilesGeometryNarrow(int outerWidth)640 void Viewport::updateTilesGeometryNarrow(int outerWidth) {
641 	if (outerWidth <= st::groupCallNarrowMembersWidth) {
642 		updateTilesGeometryColumn(outerWidth);
643 		return;
644 	}
645 
646 	const auto y = -_scrollTop;
647 	auto sizes = base::flat_map<not_null<VideoTile*>, QSize>();
648 	sizes.reserve(_tiles.size());
649 	for (const auto &tile : _tiles) {
650 		const auto video = tile.get();
651 		const auto size = video->trackOrUserpicSize();
652 		if (size.isEmpty()) {
653 			video->hide();
654 		} else {
655 			sizes.emplace(video, size);
656 		}
657 	}
658 	if (sizes.empty()) {
659 		_fullHeight = 0;
660 		return;
661 	} else if (sizes.size() == 1) {
662 		const auto size = sizes.front().second;
663 		const auto heightMin = (outerWidth * 9) / 16;
664 		const auto heightMax = (outerWidth * 3) / 4;
665 		const auto scaled = size.scaled(
666 			QSize(outerWidth, heightMax),
667 			Qt::KeepAspectRatio);
668 		const auto height = std::max(scaled.height(), heightMin);
669 		const auto skip = st::groupCallVideoSmallSkip;
670 		setTileGeometry(sizes.front().first, { 0, y, outerWidth, height });
671 		_fullHeight = height + skip;
672 		return;
673 	}
674 	const auto min = (st::groupCallWidth
675 		- st::groupCallMembersMargin.left()
676 		- st::groupCallMembersMargin.right()
677 		- st::groupCallVideoSmallSkip) / 2;
678 	const auto square = (outerWidth - st::groupCallVideoSmallSkip) / 2;
679 	const auto skip = (outerWidth - 2 * square);
680 	const auto put = [&](not_null<VideoTile*> tile, int column, int row) {
681 		setTileGeometry(tile, {
682 			(column == 2) ? 0 : column ? (outerWidth - square) : 0,
683 			y + row * (min + skip),
684 			(column == 2) ? outerWidth : square,
685 			min,
686 		});
687 	};
688 	const auto rows = (sizes.size() + 1) / 2;
689 	if (sizes.size() == 3) {
690 		put(sizes.front().first, 2, 0);
691 		put((sizes.begin() + 1)->first, 0, 1);
692 		put((sizes.begin() + 2)->first, 1, 1);
693 	} else {
694 		auto row = 0;
695 		auto column = 0;
696 		for (const auto &[video, endpoint] : sizes) {
697 			put(video, column, row);
698 			if (column) {
699 				++row;
700 				column = (row + 1 == rows && sizes.size() % 2) ? 2 : 0;
701 			} else {
702 				column = 1;
703 			}
704 		}
705 	}
706 	_fullHeight = rows * (min + skip);
707 }
708 
updateTilesGeometryColumn(int outerWidth)709 void Viewport::updateTilesGeometryColumn(int outerWidth) {
710 	const auto y = -_scrollTop;
711 	auto top = 0;
712 	const auto layoutNext = [&](not_null<VideoTile*> tile) {
713 		const auto size = tile->trackOrUserpicSize();
714 		const auto shown = !size.isEmpty() && _large && tile != _large;
715 		const auto height = st::groupCallNarrowVideoHeight;
716 		if (!shown) {
717 			tile->hide();
718 		} else {
719 			setTileGeometry(tile, { 0, y + top, outerWidth, height });
720 			top += height + st::groupCallVideoSmallSkip;
721 		}
722 	};
723 	const auto topPeer = _large ? _large->row()->peer().get() : nullptr;
724 	const auto reorderNeeded = [&] {
725 		if (!_large) {
726 			return false;
727 		}
728 		for (const auto &tile : _tiles) {
729 			if (tile.get() != _large && tile->row()->peer() == topPeer) {
730 				return (tile.get() != _tiles.front().get())
731 					&& !tile->trackOrUserpicSize().isEmpty();
732 			}
733 		}
734 		return false;
735 	}();
736 	if (reorderNeeded) {
737 		_tilesForOrder.clear();
738 		_tilesForOrder.reserve(_tiles.size());
739 		for (const auto &tile : _tiles) {
740 			_tilesForOrder.push_back(tile.get());
741 		}
742 		ranges::stable_partition(
743 			_tilesForOrder,
744 			[&](not_null<VideoTile*> tile) {
745 				return (tile->row()->peer() == topPeer);
746 			});
747 		for (const auto &tile : _tilesForOrder) {
748 			layoutNext(tile);
749 		}
750 	} else {
751 		for (const auto &tile : _tiles) {
752 			layoutNext(tile.get());
753 		}
754 	}
755 	_fullHeight = top;
756 }
757 
setTileGeometry(not_null<VideoTile * > tile,QRect geometry)758 void Viewport::setTileGeometry(not_null<VideoTile*> tile, QRect geometry) {
759 	tile->setGeometry(geometry);
760 
761 	const auto min = std::min(geometry.width(), geometry.height());
762 	const auto kMedium = style::ConvertScale(540);
763 	const auto kSmall = style::ConvertScale(240);
764 	const auto &endpoint = tile->endpoint();
765 	const auto forceThumbnailQuality = !wide()
766 		&& (ranges::count(_tiles, false, &VideoTile::hidden) > 1);
767 	const auto forceFullQuality = wide() && (tile.get() == _large);
768 	const auto quality = forceThumbnailQuality
769 		? VideoQuality::Thumbnail
770 		: (forceFullQuality || min >= kMedium)
771 		? VideoQuality::Full
772 		: (min >= kSmall)
773 		? VideoQuality::Medium
774 		: VideoQuality::Thumbnail;
775 	if (tile->updateRequestedQuality(quality)) {
776 		_qualityRequests.fire(VideoQualityRequest{
777 			.endpoint = endpoint,
778 			.quality = quality,
779 		});
780 	}
781 }
782 
setSelected(Selection value)783 void Viewport::setSelected(Selection value) {
784 	if (_selected == value) {
785 		return;
786 	}
787 	if (_selected.tile) {
788 		_selected.tile->toggleTopControlsShown(false);
789 	}
790 	_selected = value;
791 	updateTopControlsVisibility();
792 	updateCursor();
793 }
794 
updateCursor()795 void Viewport::updateCursor() {
796 	const auto pointer = _selected.tile && (!wide() || _hasTwoOrMore);
797 	widget()->setCursor(pointer ? style::cur_pointer : style::cur_default);
798 }
799 
setPressed(Selection value)800 void Viewport::setPressed(Selection value) {
801 	if (_pressed == value) {
802 		return;
803 	}
804 	_pressed = value;
805 }
806 
chooseRenderer(Ui::GL::Backend backend)807 Ui::GL::ChosenRenderer Viewport::chooseRenderer(Ui::GL::Backend backend) {
808 	_opengl = (backend == Ui::GL::Backend::OpenGL);
809 	return {
810 		.renderer = (_opengl
811 			? std::unique_ptr<Ui::GL::Renderer>(
812 				std::make_unique<RendererGL>(this))
813 			: std::make_unique<RendererSW>(this)),
814 		.backend = backend,
815 	};
816 }
817 
requireARGB32() const818 bool Viewport::requireARGB32() const {
819 	return !_opengl;
820 }
821 
fullHeight() const822 int Viewport::fullHeight() const {
823 	return _fullHeight.current();
824 }
825 
fullHeightValue() const826 rpl::producer<int> Viewport::fullHeightValue() const {
827 	return _fullHeight.value();
828 }
829 
pinToggled() const830 rpl::producer<bool> Viewport::pinToggled() const {
831 	return _pinToggles.events();
832 }
833 
clicks() const834 rpl::producer<VideoEndpoint> Viewport::clicks() const {
835 	return _clicks.events();
836 }
837 
qualityRequests() const838 rpl::producer<VideoQualityRequest> Viewport::qualityRequests() const {
839 	return _qualityRequests.events();
840 }
841 
mouseInsideValue() const842 rpl::producer<bool> Viewport::mouseInsideValue() const {
843 	return _mouseInside.value();
844 }
845 
lifetime()846 rpl::lifetime &Viewport::lifetime() {
847 	return _content->lifetime();
848 }
849 
MuteButtonTooltip(not_null<GroupCall * > call)850 rpl::producer<QString> MuteButtonTooltip(not_null<GroupCall*> call) {
851 	//return rpl::single(std::make_tuple(
852 	//	(Data::GroupCall*)nullptr,
853 	//	call->scheduleDate()
854 	//)) | rpl::then(call->real(
855 	//) | rpl::map([](not_null<Data::GroupCall*> real) {
856 	//	using namespace rpl::mappers;
857 	//	return real->scheduleDateValue(
858 	//	) | rpl::map([=](TimeId scheduleDate) {
859 	//		return std::make_tuple(real.get(), scheduleDate);
860 	//	});
861 	//}) | rpl::flatten_latest(
862 	//)) | rpl::map([=](
863 	//		Data::GroupCall *real,
864 	//		TimeId scheduleDate) -> rpl::producer<QString> {
865 	//	if (scheduleDate) {
866 	//		return rpl::combine(
867 	//			call->canManageValue(),
868 	//			(real
869 	//				? real->scheduleStartSubscribedValue()
870 	//				: rpl::single(false))
871 	//		) | rpl::map([](bool canManage, bool subscribed) {
872 	//			return canManage
873 	//				? tr::lng_group_call_start_now()
874 	//				: subscribed
875 	//				? tr::lng_group_call_cancel_reminder()
876 	//				: tr::lng_group_call_set_reminder();
877 	//		}) | rpl::flatten_latest();
878 	//	}
879 		return call->mutedValue(
880 		) | rpl::map([](MuteState muted) {
881 			switch (muted) {
882 			case MuteState::Active:
883 			case MuteState::PushToTalk:
884 				return tr::lng_group_call_you_are_live();
885 			case MuteState::ForceMuted:
886 				return tr::lng_group_call_tooltip_force_muted();
887 			case MuteState::RaisedHand:
888 				return tr::lng_group_call_tooltip_raised_hand();
889 			case MuteState::Muted:
890 				return tr::lng_group_call_tooltip_microphone();
891 			}
892 			Unexpected("Value in MuteState in showNiceTooltip.");
893 		}) | rpl::flatten_latest();
894 	//}) | rpl::flatten_latest();
895 }
896 
897 } // namespace Calls::Group
898