Improve volume slider design in group calls.

This commit is contained in:
John Preston 2021-06-17 16:22:51 +04:00
parent 55e494f55a
commit ec234cdc43
6 changed files with 139 additions and 65 deletions

View File

@ -490,6 +490,7 @@ groupCallMenu: Menu(defaultMenu) {
itemFgShortcutDisabled: groupCallMemberNotJoinedStatus;
separatorFg: groupCallMenuBgOver;
separatorPadding: margins(0px, 4px, 0px, 4px);
arrow: icon {{ "dropdown_submenu_arrow", groupCallMemberNotJoinedStatus }};
@ -513,6 +514,12 @@ groupCallPopupMenu: PopupMenu(defaultPopupMenu) {
menu: groupCallMenu;
animation: groupCallPanelAnimation;
}
groupCallPopupMenuWithVolume: PopupMenu(groupCallPopupMenu) {
scrollPadding: margins(0px, 3px, 0px, 8px);
menu: Menu(groupCallMenu) {
widthMin: 210px;
}
}
groupCallRecordingTimerPadding: margins(0px, 4px, 0px, 4px);
groupCallRecordingTimerFont: font(12px);
@ -1071,6 +1078,8 @@ groupCallMuteCrossLine: CrossLineAnimation {
groupCallMenuSpeakerArcsSkip: 1px;
groupCallMenuVolumeSkip: 5px;
groupCallMenuVolumePadding: margins(17px, 6px, 17px, 5px);
groupCallMenuVolumeMargin: margins(55px, 0px, 15px, 0px);
groupCallMenuVolumeSlider: MediaSlider(defaultContinuousSlider) {
activeFg: groupCallMembersFg;
inactiveFg: groupCallMemberInactiveIcon;
@ -1078,6 +1087,8 @@ groupCallMenuVolumeSlider: MediaSlider(defaultContinuousSlider) {
inactiveFgOver: groupCallMemberInactiveIcon;
activeFgDisabled: groupCallMemberInactiveIcon;
receivedTillFg: groupCallMemberInactiveIcon;
width: 7px;
seekSize: size(7px, 7px);
}
groupCallSpeakerArcsAnimation: ArcsAnimation {

View File

@ -1236,12 +1236,10 @@ base::unique_qptr<Ui::PopupMenu> Members::Controller::createRowContextMenu(
not_null<PeerListRow*> row) {
const auto participantPeer = row->peer();
const auto real = static_cast<Row*>(row.get());
auto result = base::make_unique_q<Ui::PopupMenu>(
parent,
st::groupCallPopupMenu);
const auto muteState = real->state();
const auto muted = (muteState == Row::State::Muted)
|| (muteState == Row::State::RaisedHand);
const auto addVolumeItem = !muted || isMe(participantPeer);
const auto admin = IsGroupCallAdmin(_peer, participantPeer);
const auto session = &_peer->session();
const auto getCurrentWindow = [=]() -> Window::SessionController* {
@ -1262,6 +1260,12 @@ base::unique_qptr<Ui::PopupMenu> Members::Controller::createRowContextMenu(
}
return getCurrentWindow();
};
auto result = base::make_unique_q<Ui::PopupMenu>(
parent,
(addVolumeItem
? st::groupCallPopupMenuWithVolume
: st::groupCallPopupMenu));
const auto weakMenu = Ui::MakeWeak(result.get());
const auto performOnMainWindow = [=](auto callback) {
if (const auto window = getWindow()) {
@ -1442,7 +1446,8 @@ void Members::Controller::addMuteActionsToContextMenu(
auto mutesFromVolume = rpl::never<bool>() | rpl::type_erased();
if (!muted || _call->joinAs() == participantPeer) {
const auto addVolumeItem = !muted || isMe(participantPeer);
if (addVolumeItem) {
auto otherParticipantStateValue
= _call->otherParticipantStateValue(
) | rpl::filter([=](const Group::ParticipantState &data) {
@ -1451,7 +1456,7 @@ void Members::Controller::addMuteActionsToContextMenu(
auto volumeItem = base::make_unique_q<MenuVolumeItem>(
menu->menu(),
st::groupCallPopupMenu.menu,
st::groupCallPopupMenuWithVolume.menu,
otherParticipantStateValue,
row->volume(),
Group::kMaxVolume,
@ -1491,6 +1496,10 @@ void Members::Controller::addMuteActionsToContextMenu(
}, volumeItem->lifetime());
menu->addAction(std::move(volumeItem));
if (!isMe(participantPeer)) {
menu->addSeparator();
}
};
const auto muteAction = [&]() -> QAction* {

View File

@ -36,10 +36,6 @@ constexpr auto kVolumeStickedValues =
{ 175. / kMaxVolumePercent, 2. / kMaxVolumePercent },
}};
QString VolumeString(int volumePercent) {
return u"%1%"_q.arg(volumePercent);
}
} // namespace
MenuVolumeItem::MenuVolumeItem(
@ -75,20 +71,21 @@ MenuVolumeItem::MenuVolumeItem(
sizeValue(
) | rpl::start_with_next([=](const QSize &size) {
const auto geometry = QRect(QPoint(), size);
_itemRect = geometry - _st.itemPadding;
_itemRect = geometry - st::groupCallMenuVolumePadding;
_speakerRect = QRect(_itemRect.topLeft(), _stCross.icon.size());
_arcPosition = _speakerRect.center()
+ QPoint(0, st::groupCallMenuSpeakerArcsSkip);
_volumeRect = QRect(
_arcPosition.x()
+ st::groupCallMenuVolumeSkip
+ _arcs->finishedWidth(),
const auto sliderLeft = _arcPosition.x()
+ st::groupCallMenuVolumeSkip
+ _arcs->maxWidth()
+ st::groupCallMenuVolumeSkip;
_slider->setGeometry(
st::groupCallMenuVolumeMargin.left(),
_speakerRect.y(),
_st.itemStyle.font->width(VolumeString(kMaxVolumePercent)),
(geometry.width()
- st::groupCallMenuVolumeMargin.left()
- st::groupCallMenuVolumeMargin.right()),
_speakerRect.height());
_slider->setGeometry(_itemRect
- style::margins(0, contentHeight() / 2, 0, 0));
}, lifetime());
setCloudVolume(startVolume);
@ -110,15 +107,12 @@ MenuVolumeItem::MenuVolumeItem(
unmuteColor(),
muteColor(),
muteProgress);
p.setPen(mutePen);
p.setFont(_st.itemStyle.font);
p.drawText(_volumeRect, VolumeString(volume), style::al_left);
_crossLineMute->paint(
p,
_speakerRect.topLeft(),
muteProgress,
(!muteProgress) ? std::nullopt : std::optional<QColor>(mutePen));
(muteProgress > 0) ? std::make_optional(mutePen) : std::nullopt);
{
p.translate(_arcPosition);
@ -133,7 +127,7 @@ MenuVolumeItem::MenuVolumeItem(
_toggleMuteLocallyRequests.fire_copy(newMuted);
_crossLineAnimation.start(
[=] { update(_speakerRect.united(_volumeRect)); },
[=] { update(_speakerRect); },
_localMuted ? 0. : 1.,
_localMuted ? 1. : 0.,
st::callPanelDuration);
@ -141,8 +135,8 @@ MenuVolumeItem::MenuVolumeItem(
if (value > 0) {
_changeVolumeLocallyRequests.fire(value * _maxVolume);
}
update(_volumeRect);
_arcs->setValue(value);
updateSliderColor(value);
});
const auto returnVolume = [=] {
@ -169,6 +163,7 @@ MenuVolumeItem::MenuVolumeItem(
if (!_cloudMuted && !muted) {
_changeVolumeRequests.fire_copy(newVolume);
}
updateSliderColor(value);
});
std::move(
@ -209,30 +204,15 @@ MenuVolumeItem::MenuVolumeItem(
}
void MenuVolumeItem::initArcsAnimation() {
const auto volumeLeftWas = lifetime().make_state<int>(0);
const auto lastTime = lifetime().make_state<int>(0);
_arcsAnimation.init([=](crl::time now) {
_arcs->update(now);
update(_speakerRect);
const auto wasRect = _volumeRect;
_volumeRect.moveLeft(anim::interpolate(
*volumeLeftWas,
_arcPosition.x()
+ st::groupCallMenuVolumeSkip
+ _arcs->finishedWidth(),
std::clamp(
(now - (*lastTime))
/ float64(st::groupCallSpeakerArcsAnimation.duration),
0.,
1.)));
update(_speakerRect.united(wasRect.united(_volumeRect)));
});
_arcs->startUpdateRequests(
) | rpl::start_with_next([=] {
if (!_arcsAnimation.animating()) {
*volumeLeftWas = _volumeRect.left();
*lastTime = crl::now();
_arcsAnimation.start();
}
@ -269,8 +249,30 @@ void MenuVolumeItem::setCloudVolume(int volume) {
}
void MenuVolumeItem::setSliderVolume(int volume) {
_slider->setValue(float64(volume) / _maxVolume);
update(_volumeRect);
const auto value = float64(volume) / _maxVolume;
_slider->setValue(value);
updateSliderColor(value);
}
void MenuVolumeItem::updateSliderColor(float64 value) {
value = std::clamp(value, 0., 1.);
const auto color = [](int rgb) {
return QColor(
int((rgb & 0xFF0000) >> 16),
int((rgb & 0x00FF00) >> 8),
int(rgb & 0x0000FF));
};
const auto colors = std::array<QColor, 4>{ {
color(0xF66464),
color(0xD0B738),
color(0x24CD80),
color(0x3BBCEC),
} };
_slider->setActiveFgOverride((value < 0.25)
? anim::color(colors[0], colors[1], value / 0.25)
: (value < 0.5)
? anim::color(colors[1], colors[2], (value - 0.25) / 0.25)
: anim::color(colors[2], colors[3], (value - 0.5) / 0.5));
}
not_null<QAction*> MenuVolumeItem::action() const {
@ -282,9 +284,9 @@ bool MenuVolumeItem::isEnabled() const {
}
int MenuVolumeItem::contentHeight() const {
return _st.itemPadding.top()
+ _st.itemPadding.bottom()
+ _stCross.icon.height() * 2;
return st::groupCallMenuVolumePadding.top()
+ st::groupCallMenuVolumePadding.bottom()
+ _stCross.icon.height();
}
rpl::producer<bool> MenuVolumeItem::toggleMuteRequests() const {

View File

@ -52,6 +52,7 @@ private:
void setCloudVolume(int volume);
void setSliderVolume(int volume);
void updateSliderColor(float64 value);
QColor unmuteColor() const;
QColor muteColor() const;
@ -64,7 +65,6 @@ private:
QRect _itemRect;
QRect _speakerRect;
QRect _volumeRect;
QPoint _arcPosition;
const base::unique_qptr<Ui::MediaSlider> _slider;

View File

@ -223,6 +223,11 @@ void MediaSlider::addDivider(float64 atValue, const QSize &size) {
_dividers.push_back(Divider{ atValue, size });
}
void MediaSlider::setActiveFgOverride(std::optional<QColor> color) {
_activeFgOverride = color;
update();
}
void MediaSlider::paintEvent(QPaintEvent *e) {
if (_paintDisabled) {
return;
@ -250,17 +255,27 @@ void MediaSlider::paintEvent(QPaintEvent *e) {
: value;
const auto markerFrom = (horizontal ? seekRect.x() : seekRect.y());
const auto markerLength = (horizontal ? seekRect.width() : seekRect.height());
const auto markerLength = horizontal
? seekRect.width()
: seekRect.height();
const auto from = 0;
const auto length = (horizontal ? width() : height());
const auto mid = qRound(from + value * length);
const auto till = std::max(mid, qRound(from + receivedTill * length));
const auto end = from + length;
const auto activeFg = disabled ? _st.activeFgDisabled : anim::brush(_st.activeFg, _st.activeFgOver, over);
const auto activeFg = disabled
? _st.activeFgDisabled
: _activeFgOverride
? QBrush(*_activeFgOverride)
: anim::brush(_st.activeFg, _st.activeFgOver, over);
const auto receivedTillFg = _st.receivedTillFg;
const auto inactiveFg = disabled ? _st.inactiveFgDisabled : anim::brush(_st.inactiveFg, _st.inactiveFgOver, over);
const auto inactiveFg = disabled
? _st.inactiveFgDisabled
: anim::brush(_st.inactiveFg, _st.inactiveFgOver, over);
if (mid > from) {
const auto fromClipRect = horizontal ? QRect(0, 0, mid, height()) : QRect(0, 0, width(), mid);
const auto fromClipRect = horizontal
? QRect(0, 0, mid, height())
: QRect(0, 0, width(), mid);
const auto till = std::min(mid + radius, end);
const auto fromRect = horizontal
? QRect(from, (height() - _st.width) / 2, till - from, _st.width)
@ -274,17 +289,31 @@ void MediaSlider::paintEvent(QPaintEvent *e) {
auto clipRect = QRect(mid, 0, till - mid, height());
const auto left = std::max(mid - radius, from);
const auto right = std::min(till + radius, end);
const auto rect = QRect(left, (height() - _st.width) / 2, right - left, _st.width);
const auto rect = QRect(
left,
(height() - _st.width) / 2,
right - left,
_st.width);
p.setClipRect(clipRect);
p.setBrush(receivedTillFg);
p.drawRoundedRect(rect, radius, radius);
}
if (end > till) {
const auto endClipRect = horizontal ? QRect(till, 0, width() - till, height()) : QRect(0, till, width(), height() - till);
const auto endClipRect = horizontal
? QRect(till, 0, width() - till, height())
: QRect(0, till, width(), height() - till);
const auto begin = std::max(till - radius, from);
const auto endRect = horizontal
? QRect(begin, (height() - _st.width) / 2, end - begin, _st.width)
: QRect((width() - _st.width) / 2, begin, _st.width, end - begin);
? QRect(
begin,
(height() - _st.width) / 2,
end - begin,
_st.width)
: QRect(
(width() - _st.width) / 2,
begin,
_st.width,
end - begin);
p.setClipRect(endClipRect);
p.setBrush(horizontal ? inactiveFg : activeFg);
p.drawRoundedRect(endRect, radius, radius);
@ -316,24 +345,46 @@ void MediaSlider::paintEvent(QPaintEvent *e) {
p.drawRoundedRect(rect, dividerRadius, dividerRadius);
}
}
const auto markerSizeRatio = disabled ? 0. : (_alwaysDisplayMarker ? 1. : over);
const auto markerSizeRatio = disabled
? 0.
: (_alwaysDisplayMarker ? 1. : over);
if (markerSizeRatio > 0) {
const auto position = qRound(markerFrom + value * markerLength) - (horizontal ? (_st.seekSize.width() / 2) : (_st.seekSize.height() / 2));
const auto position = qRound(markerFrom + value * markerLength)
- (horizontal
? (_st.seekSize.width() / 2)
: (_st.seekSize.height() / 2));
const auto seekButton = horizontal
? QRect(position, (height() - _st.seekSize.height()) / 2, _st.seekSize.width(), _st.seekSize.height())
: QRect((width() - _st.seekSize.width()) / 2, position, _st.seekSize.width(), _st.seekSize.height());
const auto size = horizontal ? _st.seekSize.width() : _st.seekSize.height();
const auto remove = static_cast<int>(((1. - markerSizeRatio) * size) / 2.);
? QRect(
position,
(height() - _st.seekSize.height()) / 2,
_st.seekSize.width(),
_st.seekSize.height())
: QRect(
(width() - _st.seekSize.width()) / 2,
position,
_st.seekSize.width(),
_st.seekSize.height());
const auto size = horizontal
? _st.seekSize.width()
: _st.seekSize.height();
const auto remove = static_cast<int>(
((1. - markerSizeRatio) * size) / 2.);
if (remove * 2 < size) {
p.setClipRect(rect());
p.setBrush(activeFg);
const auto xshift = horizontal
? std::max(seekButton.x() + seekButton.width() - remove - width(), 0) + std::min(seekButton.x() + remove, 0)
? std::max(
seekButton.x() + seekButton.width() - remove - width(),
0) + std::min(seekButton.x() + remove, 0)
: 0;
const auto yshift = horizontal
? 0
: std::max(seekButton.y() + seekButton.height() - remove - height(), 0) + std::min(seekButton.y() + remove, 0);
p.drawEllipse(seekButton.marginsRemoved(QMargins(remove, remove, remove, remove)).translated(-xshift, -yshift));
: std::max(
seekButton.y() + seekButton.height() - remove - height(),
0) + std::min(seekButton.y() + remove, 0);
p.drawEllipse(seekButton.marginsRemoved(
QMargins(remove, remove, remove, remove)
).translated(-xshift, -yshift));
}
}
}

View File

@ -178,7 +178,7 @@ public:
callback(convert(index));
});
}
void setActiveFgOverride(std::optional<QColor> color);
void addDivider(float64 atValue, const QSize &size);
protected:
@ -198,6 +198,7 @@ private:
bool _paintDisabled = false;
std::vector<Divider> _dividers;
std::optional<QColor> _activeFgOverride;
};