Improve design for all controls states.

This commit is contained in:
John Preston 2020-08-07 17:28:41 +04:00
parent d4b8fa70a7
commit 4971e281fa
7 changed files with 274 additions and 98 deletions

View file

@ -18,8 +18,6 @@ CallSignalBars {
inactiveOpacity: double;
}
callWidth: 960px;
callHeight: 540px;
callRadius: 6px;
callShadow: Shadow {
left: icon {{ "call_shadow_left", windowShadowFg }};
@ -33,22 +31,60 @@ callShadow: Shadow {
extend: margins(9px, 8px, 9px, 10px);
fallback: windowShadowFgFallback;
}
callPhotoSize: 180px;
callPhotoSmallSize: 100px;
callOutgoingPreviewSize: size(340px, 180px);
callWidthMin: 380px;
callHeightMin: 440px;
callWidth: 960px;
callHeight: 540px;
callBottomControlsHeight: 85px;
CallBodyLayout {
height: pixels;
photoTop: pixels;
photoSize: pixels;
nameTop: pixels;
statusTop: pixels;
}
callBodyLayout: CallBodyLayout {
height: 284px;
photoTop: 21px;
photoSize: 180px;
nameTop: 221px;
statusTop: 254px;
}
callBodyWithPreview: CallBodyLayout {
height: 185px;
photoTop: 21px;
photoSize: 100px;
nameTop: 132px;
statusTop: 163px;
}
callOutgoingPreviewMin: size(360px, 120px);
callOutgoingPreview: size(540px, 180px); // default, for height == callHeight.
callOutgoingPreviewMax: size(1080px, 360px);
callOutgoingDefaultSize: size(160px, 110px);
callFingerprintPadding: margins(9px, 4px, 9px, 5px);
callFingerprintTop: 11px;
callFingerprintSkip: 3px;
callFingerprintBottom: -16px;
callButton: IconButton {
width: 72px;
height: 72px;
width: 64px;
height: 64px;
iconPosition: point(-1px, -1px);
rippleAreaPosition: point(12px, 12px);
rippleAreaSize: 48px;
rippleAreaPosition: point(10px, 10px);
rippleAreaSize: 44px;
ripple: defaultRippleAnimation;
}
callButtonLabel: FlatLabel(defaultFlatLabel) {
textFg: callNameFg;
}
callAnswer: CallButton {
button: IconButton(callButton) {
@ -61,6 +97,7 @@ callAnswer: CallButton {
angle: 135.;
outerRadius: 12px;
outerBg: callAnswerBgOuter;
label: callButtonLabel;
}
callHangup: CallButton {
button: IconButton(callButton) {
@ -71,6 +108,7 @@ callHangup: CallButton {
}
bg: callHangupBg;
outerBg: callHangupBg;
label: callButtonLabel;
}
callCancel: CallButton {
button: IconButton(callButton) {
@ -81,27 +119,30 @@ callCancel: CallButton {
}
bg: callCancelBg;
outerBg: callCancelBg;
label: callButtonLabel;
}
callMuteToggle: IconButton(callButton) {
icon: icon {{ "call_record_active", callIconFg }};
ripple: RippleAnimation(defaultRippleAnimation) {
color: callMuteRipple;
callMuteToggle: CallButton {
button: IconButton(callButton) {
icon: icon {{ "call_record_active", callIconFg }};
ripple: RippleAnimation(defaultRippleAnimation) {
color: callMuteRipple;
}
}
bg: callMuteRipple;
outerBg: callMuteRipple;
label: callButtonLabel;
}
callUnmuteIcon: icon {{ "call_record_muted", callIconFg }};
callCameraToggle: IconButton(callButton) {
icon: icon {{ "call_camera_active", callIconFg }};
ripple: RippleAnimation(defaultRippleAnimation) {
color: callMuteRipple;
callCameraToggle: CallButton(callMuteToggle) {
button: IconButton(callButton) {
icon: icon {{ "call_camera_active", callIconFg }};
ripple: RippleAnimation(defaultRippleAnimation) {
color: callMuteRipple;
}
}
}
callNoCameraIcon: icon {{ "call_camera_muted", callIconFg }};
callControlsTop: 460px;
callControlsSkip: 0px;
callMuteRight: 8px;
callNameTop: 15px;
callName: FlatLabel(defaultFlatLabel) {
minWidth: 260px;
maxHeight: 30px;
@ -113,7 +154,6 @@ callName: FlatLabel(defaultFlatLabel) {
linkFontOver: font(21px semibold underline);
}
}
callStatusTop: 46px;
callStatus: FlatLabel(defaultFlatLabel) {
minWidth: 260px;
maxHeight: 20px;
@ -126,10 +166,6 @@ callStatus: FlatLabel(defaultFlatLabel) {
}
}
callFingerprintPadding: margins(9px, 4px, 9px, 5px);
callFingerprintSkip: 3px;
callFingerprintBottom: 8px;
callBarHeight: 38px;
callBarMuteToggle: IconButton {
width: 41px;

View file

@ -57,6 +57,8 @@ public:
void setProgress(float64 progress);
void setOuterValue(float64 value);
void setIconOverride(const style::icon *iconOverride);
protected:
void paintEvent(QPaintEvent *e) override;
@ -73,6 +75,8 @@ private:
const style::CallButton *_stTo = nullptr;
float64 _progress = 0.;
const style::icon *_iconOverride = nullptr;
QImage _bgMask, _bg;
QPixmap _bgFrom, _bgTo;
QImage _iconMixedMask, _iconFrom, _iconTo, _iconMixed;
@ -130,6 +134,11 @@ void Panel::Button::setOuterValue(float64 value) {
}
}
void Panel::Button::setIconOverride(const style::icon *iconOverride) {
_iconOverride = iconOverride;
update();
}
void Panel::Button::setProgress(float64 progress) {
_progress = progress;
update();
@ -183,7 +192,8 @@ void Panel::Button::paintEvent(QPaintEvent *e) {
auto positionFrom = iconPosition(_stFrom);
if (paintFrom) {
_stFrom->button.icon.paint(p, positionFrom, width());
const auto icon = _iconOverride ? _iconOverride : &_stFrom->button.icon;
icon->paint(p, positionFrom, width());
} else {
auto positionTo = iconPosition(_stTo);
if (paintTo) {
@ -248,6 +258,7 @@ Panel::Panel(not_null<Call*> call)
: RpWidget(Core::App().getModalParent())
, _call(call)
, _user(call->user())
, _bodySt(&st::callBodyLayout)
, _answerHangupRedial(this, st::callAnswer, &st::callHangup)
, _decline(this, object_ptr<Button>(this, st::callHangup))
, _cancel(this, object_ptr<Button>(this, st::callCancel))
@ -381,8 +392,6 @@ void Panel::reinitWithCall(Call *call) {
_outgoingVideoBubble = std::make_unique<VideoBubble>(
this,
_call->videoOutgoing());
_outgoingVideoBubble->setSizeConstraints(
st::callOutgoingPreviewSize);
_call->mutedValue(
) | rpl::start_with_next([=](bool mute) {
@ -414,6 +423,19 @@ void Panel::reinitWithCall(Call *call) {
checkForInactiveShow();
}, _callLifetime);
rpl::combine(
_call->stateValue(),
_call->videoOutgoing()->renderNextFrame()
) | rpl::start_with_next([=](State state, auto) {
if (state != State::Ended
&& state != State::EndedByOtherDevice
&& state != State::Failed
&& state != State::FailedHangingUp
&& state != State::HangingUp) {
refreshOutgoingPreviewInBody(state);
}
}, _callLifetime);
_name->setText(_user->name);
updateStatusText(_call->state());
}
@ -512,8 +534,6 @@ void Panel::initGeometry() {
_useTransparency = Ui::Platform::TranslucentWindowsSupported(center);
setAttribute(Qt::WA_OpaquePaintEvent, !_useTransparency);
_padding = _useTransparency ? st::callShadow.extend : style::margins(st::lineWidth, st::lineWidth, st::lineWidth, st::lineWidth);
_controlsTop = _padding.top() + st::callControlsTop;
_contentTop = _padding.top() + 2 * st::callPhotoSize;
const auto rect = [&] {
const QRect initRect(0, 0, st::callWidth, st::callHeight);
return initRect.translated(center - initRect.center()).marginsAdded(_padding);
@ -525,6 +545,18 @@ void Panel::initGeometry() {
updateControlsGeometry();
}
void Panel::refreshOutgoingPreviewInBody(State state) {
const auto inBody = (state != State::Established)
&& (_call->videoOutgoing()->state() != webrtc::VideoState::Inactive)
&& !_call->videoOutgoing()->frameSize().isEmpty();
if (_outgoingPreviewInBody == inBody) {
return;
}
_outgoingPreviewInBody = inBody;
_bodySt = inBody ? &st::callBodyWithPreview : &st::callBodyLayout;
updateControlsGeometry();
}
void Panel::createBottomImage() {
if (!_useTransparency) {
return;
@ -571,22 +603,54 @@ void Panel::resizeEvent(QResizeEvent *e) {
}
void Panel::updateControlsGeometry() {
const auto size = st::callPhotoSize;
_userpic->setGeometry((width() - size) / 2, st::callPhotoSize, size);
_name->moveToLeft((width() - _name->width()) / 2, _contentTop + st::callNameTop);
const auto innerHeight = height() - _padding.top() - _padding.bottom();
const auto availableTop = _padding.top() + _fingerprintHeight;
const auto available = height()
- (st::callBottomControlsHeight + _padding.bottom())
- availableTop;
const auto bodyPreviewSizeMax = st::callOutgoingPreviewMin
+ ((st::callOutgoingPreview
- st::callOutgoingPreviewMin)
* (innerHeight - st::callHeightMin)
/ (st::callHeight - st::callHeightMin));
const auto bodyPreviewSize = QSize(
std::min(bodyPreviewSizeMax.width(), st::callOutgoingPreviewMax.width()),
std::min(bodyPreviewSizeMax.height(), st::callOutgoingPreviewMax.height()));
const auto contentHeight = _bodySt->height
+ (_outgoingPreviewInBody ? bodyPreviewSize.height() : 0);
const auto remainingHeight = available - contentHeight;
const auto skipHeight = remainingHeight
/ (_outgoingPreviewInBody ? 3 : 2);
_bodyTop = availableTop + skipHeight;
_buttonsTop = availableTop + available;
const auto previewTop = _bodyTop + _bodySt->height + skipHeight;
_userpic->setGeometry(
(width() - _bodySt->photoSize) / 2,
_bodyTop + _bodySt->photoTop,
_bodySt->photoSize);
_name->moveToLeft(
(width() - _name->width()) / 2,
_bodyTop + _bodySt->nameTop);
updateStatusGeometry();
_outgoingVideoBubble->setBoundingRect({
(width() - st::callOutgoingPreviewSize.width()) / 2,
_contentTop + st::callStatusTop + _status->height(),
st::callOutgoingPreviewSize.width(),
st::callOutgoingPreviewSize.height()
});
if (_outgoingPreviewInBody) {
_outgoingVideoBubble->updateGeometry(
VideoBubble::DragMode::None,
QRect(
(width() - bodyPreviewSize.width()) / 2,
previewTop,
bodyPreviewSize.width(),
bodyPreviewSize.height()));
} else {
updateOutgoingVideoBubbleGeometry();
}
auto controlsTop = _padding.top() + st::callControlsTop;
auto bothWidth = _answerHangupRedial->width() + st::callControlsSkip + st::callCancel.button.width;
_decline->moveToLeft((width() - bothWidth) / 2, controlsTop);
_cancel->moveToLeft((width() - bothWidth) / 2, controlsTop);
auto bothWidth = _answerHangupRedial->width() + st::callCancel.button.width;
_decline->moveToLeft((width() - bothWidth) / 2, _buttonsTop);
_cancel->moveToLeft((width() - bothWidth) / 2, _buttonsTop);
updateHangupGeometry();
@ -597,22 +661,37 @@ void Panel::updateControlsGeometry() {
_padding.top() + skip + delta / 2);
}
void Panel::updateOutgoingVideoBubbleGeometry() {
Expects(!_outgoingPreviewInBody);
const auto size = st::callOutgoingDefaultSize;
const auto availableHeight = height() - st::callBottomControlsHeight;
const auto padding = 2 * _padding;
_outgoingVideoBubble->updateGeometry(
VideoBubble::DragMode::SnapToCorners,
QRect(
padding.left(),
padding.top(),
width() - padding.left() - padding.right(),
height() - padding.left() - padding.bottom()),
size);
}
void Panel::updateHangupGeometry() {
auto singleWidth = _answerHangupRedial->width();
auto bothWidth = singleWidth + st::callControlsSkip + st::callCancel.button.width;
auto bothWidth = singleWidth + st::callCancel.button.width;
auto rightFrom = (width() - bothWidth) / 2;
auto rightTo = (width() - singleWidth) / 2;
auto hangupProgress = _hangupShownProgress.value(_hangupShown ? 1. : 0.);
auto hangupRight = anim::interpolate(rightFrom, rightTo, hangupProgress);
auto controlsTop = _padding.top() + st::callControlsTop;
_answerHangupRedial->moveToRight(hangupRight, controlsTop);
_answerHangupRedial->moveToRight(hangupRight, _buttonsTop);
_answerHangupRedial->setProgress(hangupProgress);
_mute->moveToRight(hangupRight - _mute->width(), controlsTop);
_camera->moveToLeft(hangupRight - _mute->width(), controlsTop);
_mute->moveToRight(hangupRight - _mute->width(), _buttonsTop);
_camera->moveToLeft(hangupRight - _mute->width(), _buttonsTop);
}
void Panel::updateStatusGeometry() {
_status->moveToLeft((width() - _status->width()) / 2, _contentTop + st::callStatusTop);
_status->moveToLeft((width() - _status->width()) / 2, _bodyTop + _bodySt->statusTop);
}
void Panel::paintEvent(QPaintEvent *e) {
@ -707,10 +786,6 @@ void Panel::mousePressEvent(QMouseEvent *e) {
_dragging = true;
_dragStartMousePosition = e->globalPos();
_dragStartMyPosition = QPoint(x(), y());
} else if (!rect().contains(e->pos())) {
if (_call && _call->state() == State::Established) {
hideDeactivated();
}
}
}
}
@ -833,8 +908,14 @@ void Panel::fillFingerprint() {
auto rectWidth = count * size + (count - 1) * st::callFingerprintSkip;
auto rectHeight = size;
auto left = (width() - rectWidth) / 2;
auto top = _padding.top() + st::callFingerprintBottom;
_fingerprintArea = QRect(left, top, rectWidth, rectHeight).marginsAdded(st::callFingerprintPadding);
_fingerprintArea = QRect(
left,
_padding.top() + st::callFingerprintTop + st::callFingerprintPadding.top(),
rectWidth,
rectHeight
).marginsAdded(st::callFingerprintPadding);
_fingerprintHeight = st::callFingerprintTop + _fingerprintArea.height() + st::callFingerprintBottom;
updateControlsGeometry();
update();
}

View file

@ -30,6 +30,7 @@ class FadeWrap;
namespace style {
struct CallSignalBars;
struct CallBodyLayout;
} // namespace style
namespace Calls {
@ -60,6 +61,7 @@ protected:
bool eventHook(QEvent *e) override;
private:
class Button;
using State = Call::State;
using Type = Call::Type;
@ -82,6 +84,7 @@ private:
void updateControlsGeometry();
void updateHangupGeometry();
void updateStatusGeometry();
void updateOutgoingVideoBubbleGeometry();
void stateChanged(State state);
void showControls();
void updateStatusText(State state);
@ -95,6 +98,7 @@ private:
[[nodiscard]] bool hasActiveVideo() const;
void checkForInactiveHide();
void checkForInactiveShow();
void refreshOutgoingPreviewInBody(State state);
Call *_call = nullptr;
not_null<UserData*> _user;
@ -102,8 +106,6 @@ private:
bool _useTransparency = true;
bool _incomingShown = false;
style::margins _padding;
int _contentTop = 0;
int _controlsTop = 0;
bool _dragging = false;
QPoint _dragStartMousePosition;
@ -111,14 +113,15 @@ private:
rpl::lifetime _callLifetime;
class Button;
not_null<const style::CallBodyLayout*> _bodySt;
object_ptr<Button> _answerHangupRedial;
object_ptr<Ui::FadeWrap<Button>> _decline;
object_ptr<Ui::FadeWrap<Button>> _cancel;
bool _hangupShown = false;
bool _outgoingPreviewInBody = false;
Ui::Animations::Simple _hangupShownProgress;
object_ptr<Ui::IconButton> _camera;
object_ptr<Ui::IconButton> _mute;
object_ptr<Button> _camera;
object_ptr<Button> _mute;
object_ptr<Ui::FlatLabel> _name;
object_ptr<Ui::FlatLabel> _status;
object_ptr<SignalBars> _signalBars = { nullptr };
@ -126,6 +129,9 @@ private:
std::unique_ptr<VideoBubble> _outgoingVideoBubble;
std::vector<EmojiPtr> _fingerprint;
QRect _fingerprintArea;
int _bodyTop = 0;
int _buttonsTop = 0;
int _fingerprintHeight = 0;
base::Timer _updateDurationTimer;
base::Timer _updateOuterRippleTimer;

View file

@ -44,10 +44,12 @@ void Userpic::setGeometry(int x, int y, int size) {
if (this->size() != size) {
_userPhoto = QPixmap();
_userPhotoFull = false;
refreshPhoto();
}
_content.setGeometry(x, y, size, size);
_content.update();
if (_userPhoto.isNull()) {
refreshPhoto();
}
}
void Userpic::setup(rpl::producer<bool> muted) {
@ -139,28 +141,29 @@ void Userpic::refreshPhoto() {
}
void Userpic::createCache(Image *image) {
auto size = this->size() * cIntRetinaFactor();
const auto size = this->size();
const auto real = size * cIntRetinaFactor();
auto options = Images::Option::Smooth | Images::Option::Circled;
// _useTransparency ? (Images::Option::RoundedLarge | Images::Option::RoundedTopLeft | Images::Option::RoundedTopRight | Images::Option::Smooth) : Images::Option::None;
if (image) {
auto width = image->width();
auto height = image->height();
if (width > height) {
width = qMax((width * size) / height, 1);
height = size;
width = qMax((width * real) / height, 1);
height = real;
} else {
height = qMax((height * size) / width, 1);
width = size;
height = qMax((height * real) / width, 1);
width = real;
}
_userPhoto = image->pixNoCache(
width,
height,
options,
st::callPhotoSize,
st::callPhotoSize);
size,
size);
_userPhoto.setDevicePixelRatio(cRetinaFactor());
} else {
auto filled = QImage(QSize(st::callPhotoSize, st::callPhotoSize) * cIntRetinaFactor(), QImage::Format_ARGB32_Premultiplied);
auto filled = QImage(QSize(real, real), QImage::Format_ARGB32_Premultiplied);
filled.setDevicePixelRatio(cRetinaFactor());
filled.fill(Qt::transparent);
{
@ -168,7 +171,7 @@ void Userpic::createCache(Image *image) {
Ui::EmptyUserpic(
Data::PeerUserpicColor(_peer->id),
_peer->name
).paint(p, 0, 0, st::callPhotoSize, st::callPhotoSize);
).paint(p, 0, 0, size, size);
}
//Images::prepareRound(filled, ImageRoundRadius::Large, RectPart::TopLeft | RectPart::TopRight);
_userPhoto = Images::PixmapFast(std::move(filled));

View file

@ -47,15 +47,39 @@ void VideoBubble::setup() {
}, lifetime());
}
void VideoBubble::setDragMode(DragMode mode) {
void VideoBubble::updateGeometry(
DragMode mode,
QRect boundingRect,
QSize sizeMin,
QSize sizeMax) {
Expects(!boundingRect.isEmpty());
Expects(sizeMax.isEmpty() || !sizeMin.isEmpty());
Expects(sizeMax.isEmpty() || sizeMin.width() <= sizeMax.width());
Expects(sizeMax.isEmpty() || sizeMin.height() <= sizeMax.height());
if (sizeMin.isEmpty()) {
sizeMin = boundingRect.size();
}
if (sizeMax.isEmpty()) {
sizeMax = sizeMin;
}
if (_dragMode != mode) {
applyDragMode(mode);
}
if (_boundingRect != boundingRect) {
applyBoundingRect(boundingRect);
}
if (_min != sizeMin || _max != sizeMax) {
applySizeConstraints(sizeMin, sizeMax);
}
if (_geometryDirty && !_lastFrameSize.isEmpty()) {
updateSizeToFrame(base::take(_lastFrameSize));
}
}
void VideoBubble::setBoundingRect(QRect rect) {
void VideoBubble::applyBoundingRect(QRect rect) {
_boundingRect = rect;
setSizeConstraints(rect.size());
_geometryDirty = true;
}
void VideoBubble::applyDragMode(DragMode mode) {
@ -66,23 +90,21 @@ void VideoBubble::applyDragMode(DragMode mode) {
}
_content.setAttribute(
Qt::WA_TransparentForMouseEvents,
(_dragMode == DragMode::None));
}
void VideoBubble::setSizeConstraints(QSize min, QSize max) {
Expects(!min.isEmpty());
Expects(max.isEmpty() || min.width() <= max.width());
Expects(max.isEmpty() || min.height() <= max.height());
if (max.isEmpty()) {
max = min;
true/*(_dragMode == DragMode::None)*/);
if (_dragMode == DragMode::SnapToCorners) {
_corner = RectPart::BottomRight;
} else {
_corner = RectPart::None;
_lastDraggableSize = _size;
}
applySizeConstraints(min, max);
_size = QSize();
_geometryDirty = true;
}
void VideoBubble::applySizeConstraints(QSize min, QSize max) {
_min = min;
_max = max;
_geometryDirty = true;
}
void VideoBubble::paint() {
@ -114,7 +136,11 @@ void VideoBubble::updateSizeToFrame(QSize frame) {
}
_lastFrameSize = frame;
auto size = _size;
auto size = !_size.isEmpty()
? _size
: (_dragMode == DragMode::None || _lastDraggableSize.isEmpty())
? QSize()
: _lastDraggableSize;
if (size.isEmpty()) {
size = frame.scaled((_min + _max) / 2, Qt::KeepAspectRatio);
} else {
@ -130,15 +156,34 @@ void VideoBubble::updateSizeToFrame(QSize frame) {
}
void VideoBubble::setInnerSize(QSize size) {
if (_size == size) {
if (_size == size && !_geometryDirty) {
return;
}
_geometryDirty = false;
_size = size;
_content.setGeometry(
_boundingRect.x() + (_boundingRect.width() - size.width()) / 2,
_boundingRect.y() + (_boundingRect.height() - size.height()) / 2,
size.width(),
size.height());
_content.setGeometry(QRect([&] {
switch (_corner) {
case RectPart::None:
return _boundingRect.topLeft() + QPoint(
(_boundingRect.width() - size.width()) / 2,
(_boundingRect.height() - size.height()) / 2);
case RectPart::TopLeft:
return _boundingRect.topLeft();
case RectPart::TopRight:
return QPoint(
_boundingRect.x() + _boundingRect.width() - size.width(),
_boundingRect.y());
case RectPart::BottomRight:
return QPoint(
_boundingRect.x() + _boundingRect.width() - size.width(),
_boundingRect.y() + _boundingRect.height() - size.height());
case RectPart::BottomLeft:
return QPoint(
_boundingRect.x(),
_boundingRect.y() + _boundingRect.height() - size.height());
}
Unexpected("Corner value in VideoBubble::setInnerSize.");
}(), size));
}
void VideoBubble::updateVisibility() {

View file

@ -26,9 +26,11 @@ public:
None,
SnapToCorners,
};
void setDragMode(DragMode mode);
void setBoundingRect(QRect rect);
void setSizeConstraints(QSize min, QSize max = QSize());
void updateGeometry(
DragMode mode,
QRect boundingRect,
QSize sizeMin = QSize(),
QSize sizeMax = QSize());
[[nodiscard]] rpl::lifetime &lifetime() {
return _content.lifetime();
@ -39,6 +41,7 @@ private:
void paint();
void setState(webrtc::VideoState state);
void applyDragMode(DragMode mode);
void applyBoundingRect(QRect rect);
void applySizeConstraints(QSize min, QSize max);
void updateSizeToFrame(QSize frame);
void updateVisibility();
@ -48,10 +51,12 @@ private:
const not_null<webrtc::VideoTrack*> _track;
webrtc::VideoState _state = webrtc::VideoState();
QImage _pausedFrame;
QSize _min, _max, _size, _lastFrameSize;
QSize _min, _max, _size, _lastDraggableSize, _lastFrameSize;
QRect _boundingRect;
DragMode _dragMode = DragMode::None;
RectPart _corner = RectPart::None;
bool _dragging = false;
bool _geometryDirty = false;
};

@ -1 +1 @@
Subproject commit 697f2851b0625ae784e405e7fc596d1629e8668a
Subproject commit c9235ec9c25565516da04abf083e9c0500de58bc