#include "callwindow.h" #include "../transport/gui_bridge.h" #include "call_audio_engine.h" #include "sound_manager.h" #include #include #include #include #include #include #include #include #include #include #include extern "C" { #include "call/call.h" #include "../../lib/mem.h" #include "../../lib/debug_config.h" } /* ── рисование иконок звонка (QPainter, без внешних ассетов) ── */ enum class Glyph { Phone, PhoneDown, Mic, MicOff }; static QColor colorForName(const QString& name) { static const QColor palette[] = { QColor("#5B9BD5"), QColor("#70AD47"), QColor("#ED7D31"), QColor("#9B59B6"), QColor("#F4B400"), QColor("#E67E22"), QColor("#16A085"), QColor("#D35400"), }; quint32 h = 0; for (const QChar c : name) h = h * 31u + c.unicode(); return palette[h % 8]; } static QPixmap makeAvatar(const QString& name, int size) { QPixmap pm(size, size); pm.fill(Qt::transparent); QPainter p(&pm); p.setRenderHint(QPainter::Antialiasing); p.setBrush(colorForName(name)); p.setPen(Qt::NoPen); p.drawEllipse(0, 0, size, size); QString initial = name.trimmed().isEmpty() ? QStringLiteral("?") : name.trimmed().left(1).toUpper(); p.setPen(Qt::white); QFont f = p.font(); f.setBold(true); f.setPixelSize(size / 2); p.setFont(f); p.drawText(QRect(0, 0, size, size), Qt::AlignCenter, initial); p.end(); return pm; } static void drawPhoneGlyph(QPainter* p, const QRectF& r, bool down) { const qreal s = r.width(); p->save(); p->translate(r.center()); p->rotate(down ? 135.0 : -45.0); const qreal w = s * 0.36, h = s * 0.64; QPainterPath path; path.addRoundedRect(QRectF(-w / 2, -h / 2, w, h), w * 0.5, w * 0.5); p->setPen(Qt::NoPen); p->setBrush(Qt::white); p->drawPath(path); p->restore(); } static void drawMicGlyph(QPainter* p, const QRectF& r, bool off) { const qreal s = r.width(); const QPointF c = r.center(); p->save(); QPen pen(Qt::white, s * 0.09, Qt::SolidLine, Qt::RoundCap, Qt::RoundJoin); p->setPen(pen); p->setBrush(Qt::NoBrush); const qreal cw = s * 0.30; p->drawRoundedRect(QRectF(c.x() - cw / 2, r.top() + s * 0.10, cw, s * 0.40), cw / 2, cw / 2); p->drawArc(QRectF(c.x() - s * 0.20, r.top() + s * 0.42, s * 0.40, s * 0.26), 180 * 16, 180 * 16); p->drawLine(QPointF(c.x(), r.top() + s * 0.60), QPointF(c.x(), r.top() + s * 0.68)); p->drawLine(QPointF(c.x() - s * 0.17, r.top() + s * 0.68), QPointF(c.x() + s * 0.17, r.top() + s * 0.68)); if (off) { p->setPen(QPen(Qt::white, s * 0.09, Qt::SolidLine, Qt::RoundCap)); p->drawLine(QPointF(c.x() - s * 0.26, c.y() - s * 0.26), QPointF(c.x() + s * 0.26, c.y() + s * 0.26)); } p->restore(); } static QIcon makeRoundIcon(const QColor& bg, Glyph glyph, int size) { QPixmap pm(size, size); pm.fill(Qt::transparent); QPainter p(&pm); p.setRenderHint(QPainter::Antialiasing); p.setPen(Qt::NoPen); p.setBrush(bg); p.drawEllipse(0, 0, size, size); const QRectF glyphRect(size * 0.24, size * 0.24, size * 0.52, size * 0.52); switch (glyph) { case Glyph::Phone: drawPhoneGlyph(&p, glyphRect, false); break; case Glyph::PhoneDown: drawPhoneGlyph(&p, glyphRect, true); break; case Glyph::Mic: drawMicGlyph(&p, glyphRect, false); break; case Glyph::MicOff: drawMicGlyph(&p, glyphRect, true); break; } p.end(); return QIcon(pm); } static QPushButton* makeRoundBtn(const QIcon& icon, const QString& tooltip, QWidget* parent) { auto* btn = new QPushButton(parent); btn->setIcon(icon); btn->setIconSize(QSize(52, 52)); btn->setFixedSize(64, 64); btn->setCursor(Qt::PointingHandCursor); btn->setToolTip(tooltip); btn->setStyleSheet( "QPushButton { border: none; border-radius: 32px; background: transparent; }" "QPushButton:pressed { background: rgba(255,255,255,0.15); }"); return btn; } static QWidget* wrapWithCaption(QWidget* btn, const QString& caption, QLabel** captionOut = nullptr) { auto* w = new QWidget(btn->parentWidget()); auto* l = new QVBoxLayout(w); l->setContentsMargins(0, 0, 0, 0); l->setSpacing(6); l->addWidget(btn, 0, Qt::AlignHCenter); auto* cap = new QLabel(caption, w); cap->setAlignment(Qt::AlignCenter); cap->setStyleSheet("color: palette(text); font-size: 11px; background: transparent;"); l->addWidget(cap, 0, Qt::AlignHCenter); if (captionOut) *captionOut = cap; return w; } CallWindow::CallWindow(QWidget* parent) : QWidget(parent, Qt::Window) { setWindowTitle(QStringLiteral("Звонок")); setFixedSize(340, 400); setAttribute(Qt::WA_DeleteOnClose, false); m_avatarLabel = new QLabel(this); m_avatarLabel->setFixedSize(80, 80); m_avatarLabel->setAlignment(Qt::AlignCenter); m_peerLabel = new QLabel(this); m_peerLabel->setAlignment(Qt::AlignCenter); m_peerLabel->setStyleSheet("font-size: 17px; font-weight: bold; color: palette(text);"); m_statusLabel = new QLabel(this); m_statusLabel->setAlignment(Qt::AlignCenter); m_statusLabel->setStyleSheet("color: gray; font-size: 13px;"); m_durationLabel = new QLabel(QStringLiteral("00:00"), this); m_durationLabel->setAlignment(Qt::AlignCenter); m_durationLabel->setStyleSheet("font-size: 15px; color: palette(text); font-family: monospace;"); m_bgpPathLabel = new QLabel(this); m_bgpPathLabel->setAlignment(Qt::AlignCenter); m_bgpPathLabel->setWordWrap(true); m_bgpPathLabel->setStyleSheet("color: gray; font-size: 11px;"); m_bgpPathLabel->hide(); m_acceptBtn = makeRoundBtn(makeRoundIcon(QColor("#27ae60"), Glyph::Phone, 96), QStringLiteral("Принять"), this); m_declineBtn = makeRoundBtn(makeRoundIcon(QColor("#e74c3c"), Glyph::PhoneDown, 96), QStringLiteral("Отклонить"), this); m_hangupBtn = makeRoundBtn(makeRoundIcon(QColor("#e74c3c"), Glyph::PhoneDown, 96), QStringLiteral("Завершить"), this); m_muteBtn = makeRoundBtn(makeRoundIcon(QColor("#7f8c8d"), Glyph::Mic, 96), QStringLiteral("Mute"), this); m_muteBtn->setCheckable(true); m_deviceCombo = new QComboBox(this); m_deviceCombo->setStyleSheet( "QComboBox { border: 1px solid palette(mid); border-radius: 4px;" " padding: 6px 10px; font-size: 13px; background: palette(base); }"); QVBoxLayout* lay = new QVBoxLayout(this); lay->setContentsMargins(16, 20, 16, 16); lay->setSpacing(8); lay->addWidget(m_avatarLabel, 0, Qt::AlignHCenter); lay->addWidget(m_peerLabel); lay->addWidget(m_statusLabel); lay->addWidget(m_durationLabel); lay->addWidget(m_bgpPathLabel); lay->addStretch(1); m_acceptCell = wrapWithCaption(m_acceptBtn, QStringLiteral("Принять")); m_declineCell = wrapWithCaption(m_declineBtn, QStringLiteral("Отклонить")); m_hangupCell = wrapWithCaption(m_hangupBtn, QStringLiteral("Завершить")); m_muteCell = wrapWithCaption(m_muteBtn, QStringLiteral("Mute"), &m_muteCaption); QHBoxLayout* btn = new QHBoxLayout(); btn->setSpacing(10); btn->addStretch(1); btn->addWidget(m_acceptCell); btn->addWidget(m_declineCell); btn->addWidget(m_hangupCell); btn->addWidget(m_muteCell); btn->addStretch(1); lay->addLayout(btn); lay->addStretch(1); lay->addWidget(m_deviceCombo); connect(m_acceptBtn, &QPushButton::clicked, this, &CallWindow::onAccept); connect(m_declineBtn, &QPushButton::clicked, this, &CallWindow::onDecline); connect(m_hangupBtn, &QPushButton::clicked, this, &CallWindow::onHangup); connect(m_muteBtn, &QPushButton::toggled, this, &CallWindow::onMuteToggled); connect(m_deviceCombo, QOverload::of(&QComboBox::activated), this, &CallWindow::onDeviceChanged); m_durationTimer = new QTimer(this); m_durationTimer->setInterval(1000); connect(m_durationTimer, &QTimer::timeout, this, &CallWindow::updateDuration); setState(Outgoing); } CallWindow::~CallWindow() = default; void CallWindow::setPeerName(const QString& name) { m_peerLabel->setText(name); m_avatarLabel->setPixmap(makeAvatar(name, 80)); } void CallWindow::setBgpPath(const QString& path) { m_bgpPathLabel->setText(path); m_bgpPathLabel->setVisible(!path.isEmpty()); } void CallWindow::setState(State s) { m_state = s; switch (s) { case Outgoing: m_statusLabel->setText(QStringLiteral("Звоним…")); m_durationLabel->hide(); break; case Incoming: m_statusLabel->setText(QStringLiteral("Входящий звонок…")); m_durationLabel->hide(); break; case Active: m_statusLabel->setText(QStringLiteral("Разговор")); m_durationLabel->setText(QStringLiteral("00:00")); m_durationLabel->show(); m_startMs = QDateTime::currentMSecsSinceEpoch(); m_durationTimer->start(); populateDeviceCombo(); m_muteBtn->setChecked(false); break; case Ended: m_statusLabel->setText(QStringLiteral("Завершён")); m_durationTimer->stop(); break; } updateButtons(); } void CallWindow::populateDeviceCombo() { m_deviceCombo->blockSignals(true); m_deviceCombo->clear(); m_deviceCombo->addItem(QStringLiteral("Default (auto)"), -1); const auto devs = SoundManager::enumeratePlaybackDevices(); for (int i = 0; i < devs.size(); i++) { QString label = devs[i].name; if (devs[i].isDefault) label += QStringLiteral(" (default)"); m_deviceCombo->addItem(label, i); } int cur = CallAudioEngine::instance()->playbackDeviceIndex(); for (int i = 0; i < m_deviceCombo->count(); i++) { if (m_deviceCombo->itemData(i).toInt() == cur) { m_deviceCombo->setCurrentIndex(i); break; } } m_deviceCombo->blockSignals(false); } void CallWindow::updateButtons() { m_acceptCell->setVisible(m_state == Incoming); m_declineCell->setVisible(m_state == Incoming); m_hangupCell->setVisible(m_state == Outgoing || m_state == Active); m_muteCell->setVisible(m_state == Active); m_deviceCombo->setVisible(m_state == Active); } void CallWindow::postCallFn(void (*fn)(void*), uint64_t callId) { struct call_id_arg* a = (struct call_id_arg*)u_malloc(sizeof(*a)); if (!a) return; a->inst = gui_bridge_get_inst(); a->call_id = callId; gui_bridge_post_uasync_fn(fn, a); } void CallWindow::onAccept() { setState(Active); postCallFn(call_accept_trampoline, m_callId); } void CallWindow::onDecline() { postCallFn(call_decline_trampoline, m_callId); } void CallWindow::onHangup() { postCallFn(call_hangup_trampoline, m_callId); } void CallWindow::onMuteToggled(bool checked) { CallAudioEngine::instance()->setMuted(checked); m_muteBtn->setIcon(makeRoundIcon(checked ? QColor("#e74c3c") : QColor("#7f8c8d"), checked ? Glyph::MicOff : Glyph::Mic, 96)); if (m_muteCaption) m_muteCaption->setText(checked ? QStringLiteral("Вкл. микрофон") : QStringLiteral("Mute")); } void CallWindow::onDeviceChanged(int index) { int devIdx = m_deviceCombo->itemData(index).toInt(); CallAudioEngine::instance()->setPlaybackDeviceIndex(devIdx); } void CallWindow::closeEvent(QCloseEvent* event) { if (m_state == Active) postCallFn(call_hangup_trampoline, m_callId); emit closed(m_callId); event->accept(); } void CallWindow::updateDuration() { if (m_state != Active) return; qint64 ms = QDateTime::currentMSecsSinceEpoch() - m_startMs; qint64 s = ms / 1000; m_durationLabel->setText(QStringLiteral("%1:%2") .arg(s / 60, 2, 10, QChar('0')) .arg(s % 60, 2, 10, QChar('0'))); }