diff --git a/CMakeLists.txt b/CMakeLists.txt index 8300ed6..281a41f 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -96,6 +96,7 @@ if(WARPPIPE_BUILD_GUI) gui/VolumeWidgets.cpp gui/AudioLevelMeter.cpp gui/SquareConnectionPainter.cpp + gui/WarpBezierConnectionPainter.cpp ) target_link_libraries(warppipe-gui PRIVATE @@ -114,6 +115,7 @@ if(WARPPIPE_BUILD_GUI) gui/VolumeWidgets.cpp gui/AudioLevelMeter.cpp gui/SquareConnectionPainter.cpp + gui/WarpBezierConnectionPainter.cpp ) target_compile_definitions(warppipe-gui-tests PRIVATE WARPPIPE_TESTING) diff --git a/gui/GraphEditorWidget.cpp b/gui/GraphEditorWidget.cpp index 7c26fa9..57047ce 100644 --- a/gui/GraphEditorWidget.cpp +++ b/gui/GraphEditorWidget.cpp @@ -9,7 +9,7 @@ #include #include #include -#include +#include "WarpBezierConnectionPainter.h" #include #include #include @@ -1927,7 +1927,7 @@ void GraphEditorWidget::setConnectionStyle(ConnectionStyleType style) { m_scene->setConnectionPainter(std::make_unique()); } else { m_scene->setConnectionPainter( - std::make_unique()); + std::make_unique()); } for (auto *item : m_scene->items()) { diff --git a/gui/SquareConnectionPainter.cpp b/gui/SquareConnectionPainter.cpp index 5139e3e..763aabb 100644 --- a/gui/SquareConnectionPainter.cpp +++ b/gui/SquareConnectionPainter.cpp @@ -205,8 +205,7 @@ void SquareConnectionPainter::paint( auto *model = dynamic_cast(&scene->graphModel()); if (model) { auto cId = cgo.connectionId(); - peakLevel = std::max(model->nodePeakLevel(cId.outNodeId), - model->nodePeakLevel(cId.inNodeId)); + peakLevel = model->connectionPeakLevel(cId); } } diff --git a/gui/WarpBezierConnectionPainter.cpp b/gui/WarpBezierConnectionPainter.cpp new file mode 100644 index 0000000..75d23c0 --- /dev/null +++ b/gui/WarpBezierConnectionPainter.cpp @@ -0,0 +1,117 @@ +#include "WarpBezierConnectionPainter.h" +#include "WarpGraphModel.h" + +#include +#include +#include +#include +#include + +#include +#include +#include + +#include +#include + +QPainterPath WarpBezierConnectionPainter::cubicPath( + QtNodes::ConnectionGraphicsObject const &cgo) const { + QPointF const &in = cgo.endPoint(QtNodes::PortType::In); + QPointF const &out = cgo.endPoint(QtNodes::PortType::Out); + auto const c1c2 = cgo.pointsC1C2(); + + QPainterPath cubic(out); + cubic.cubicTo(c1c2.first, c1c2.second, in); + return cubic; +} + +void WarpBezierConnectionPainter::paint( + QPainter *painter, + QtNodes::ConnectionGraphicsObject const &cgo) const { + auto const &style = QtNodes::StyleCollection::connectionStyle(); + + bool const hovered = cgo.connectionState().hovered(); + bool const selected = cgo.isSelected(); + bool const sketch = cgo.connectionState().requiresPort(); + + auto path = cubicPath(cgo); + + float peakLevel = 0.0f; + auto *scene = cgo.nodeScene(); + if (scene) { + auto *model = dynamic_cast(&scene->graphModel()); + if (model) { + auto cId = cgo.connectionId(); + peakLevel = model->connectionPeakLevel(cId); + } + } + + auto activeColor = [&](QColor base) -> QColor { + if (peakLevel < 0.005f) + return base; + float t = std::min(peakLevel * 2.0f, 1.0f); + int r = static_cast(base.red() + t * (60 - base.red())); + int g = static_cast(base.green() + t * (210 - base.green())); + int b = static_cast(base.blue() + t * (80 - base.blue())); + return QColor(std::clamp(r, 0, 255), + std::clamp(g, 0, 255), + std::clamp(b, 0, 255), + base.alpha()); + }; + + if (hovered || selected) { + QPen pen; + pen.setWidth(static_cast(2 * style.lineWidth())); + pen.setColor(selected ? style.selectedHaloColor() : style.hoveredColor()); + painter->setPen(pen); + painter->setBrush(Qt::NoBrush); + painter->drawPath(path); + } + + if (sketch) { + QPen pen; + pen.setWidth(static_cast(style.constructionLineWidth())); + pen.setColor(style.constructionColor()); + pen.setStyle(Qt::DashLine); + painter->setPen(pen); + painter->setBrush(Qt::NoBrush); + painter->drawPath(path); + } else { + QColor base = selected ? style.selectedColor() : style.normalColor(); + QColor color = selected ? base : activeColor(base); + float width = style.lineWidth(); + if (!selected && peakLevel > 0.005f) + width += peakLevel * 1.5f; + + QPen pen; + pen.setWidthF(width); + pen.setColor(color); + painter->setPen(pen); + painter->setBrush(Qt::NoBrush); + painter->drawPath(path); + } + + double const pointRadius = style.pointDiameter() / 2.0; + painter->setPen(style.constructionColor()); + painter->setBrush(style.constructionColor()); + painter->drawEllipse(cgo.out(), pointRadius, pointRadius); + painter->drawEllipse(cgo.in(), pointRadius, pointRadius); +} + +QPainterPath WarpBezierConnectionPainter::getPainterStroke( + QtNodes::ConnectionGraphicsObject const &cgo) const { + auto cubic = cubicPath(cgo); + + QPointF const &out = cgo.endPoint(QtNodes::PortType::Out); + QPainterPath result(out); + + unsigned int constexpr segments = 20; + for (unsigned int i = 0; i < segments; ++i) { + double ratio = double(i + 1) / segments; + result.lineTo(cubic.pointAtPercent(ratio)); + } + + QPainterPathStroker stroker; + stroker.setWidth(10.0); + return stroker.createStroke(result); +} diff --git a/gui/WarpBezierConnectionPainter.h b/gui/WarpBezierConnectionPainter.h new file mode 100644 index 0000000..a9377a3 --- /dev/null +++ b/gui/WarpBezierConnectionPainter.h @@ -0,0 +1,15 @@ +#pragma once + +#include + +class WarpBezierConnectionPainter : public QtNodes::AbstractConnectionPainter { +public: + void paint(QPainter *painter, + QtNodes::ConnectionGraphicsObject const &cgo) const override; + QPainterPath + getPainterStroke(QtNodes::ConnectionGraphicsObject const &cgo) const override; + +private: + QPainterPath + cubicPath(QtNodes::ConnectionGraphicsObject const &cgo) const; +}; diff --git a/gui/WarpGraphModel.cpp b/gui/WarpGraphModel.cpp index e69298b..87cf2d2 100644 --- a/gui/WarpGraphModel.cpp +++ b/gui/WarpGraphModel.cpp @@ -1473,6 +1473,23 @@ float WarpGraphModel::nodePeakLevel(QtNodes::NodeId nodeId) const { return it != m_peakLevels.end() ? it->second : 0.0f; } +float WarpGraphModel::connectionPeakLevel(QtNodes::ConnectionId cId) const { + constexpr float kSourceReliableThreshold = 0.005f; + + float outPeak = nodePeakLevel(cId.outNodeId); + if (outPeak >= kSourceReliableThreshold) + return outPeak; + + auto outNodeIt = m_nodes.find(cId.outNodeId); + if (outNodeIt == m_nodes.end()) + return outPeak; + + if (classifyNode(outNodeIt->second.info) != WarpNodeType::kApplication) + return outPeak; + + return std::max(outPeak, nodePeakLevel(cId.inNodeId)); +} + void WarpGraphModel::recomputeConnectionChannels() { m_connectionChannels.clear(); diff --git a/gui/WarpGraphModel.h b/gui/WarpGraphModel.h index 1142a53..dcc2c3d 100644 --- a/gui/WarpGraphModel.h +++ b/gui/WarpGraphModel.h @@ -109,6 +109,7 @@ public: void setNodePeakLevel(QtNodes::NodeId nodeId, float level); float nodePeakLevel(QtNodes::NodeId nodeId) const; + float connectionPeakLevel(QtNodes::ConnectionId cId) const; struct ConnectionChannel { int index = 0; diff --git a/src/warppipe.cpp b/src/warppipe.cpp index 7213d75..15930a7 100644 --- a/src/warppipe.cpp +++ b/src/warppipe.cpp @@ -697,7 +697,14 @@ void Client::Impl::RegistryGlobalRemove(void* data, uint32_t id) { { std::lock_guard lock(impl->cache_mutex); impl->virtual_streams.erase(id); - impl->link_proxies.erase(id); + auto link_it = impl->link_proxies.find(id); + if (link_it != impl->link_proxies.end()) { + if (link_it->second && link_it->second->proxy) { + spa_hook_remove(&link_it->second->listener); + link_it->second->proxy = nullptr; + } + impl->link_proxies.erase(link_it); + } auto node_it = impl->nodes.find(id); if (node_it != impl->nodes.end()) { impl->nodes.erase(node_it); diff --git a/tests/gui/warppipe_gui_tests.cpp b/tests/gui/warppipe_gui_tests.cpp index fd1d227..b248f0e 100644 --- a/tests/gui/warppipe_gui_tests.cpp +++ b/tests/gui/warppipe_gui_tests.cpp @@ -1671,6 +1671,42 @@ TEST_CASE("findPwNodeIdByName returns 0 for ghost nodes without pw mapping") { REQUIRE(model.findPwNodeIdByName("ghost-lookup") == 100220); } +TEST_CASE("connectionPeakLevel uses source node activity, with application fallback") { + auto tc = TestClient::Create(); + if (!tc.available()) { SUCCEED("PipeWire unavailable"); return; } + ensureApp(); + + REQUIRE(tc.client->Test_InsertNode( + MakeNode(100240, "app-out", "Stream/Output/Audio", "Firefox")).ok()); + REQUIRE(tc.client->Test_InsertNode( + MakeNode(100241, "sink-out", "Audio/Sink")).ok()); + REQUIRE(tc.client->Test_InsertNode( + MakeNode(100242, "hw-in", "Audio/Sink")).ok()); + + WarpGraphModel model(tc.client.get()); + model.refreshFromClient(); + + auto appQt = model.qtNodeIdForPw(100240); + auto sinkQt = model.qtNodeIdForPw(100241); + auto hwQt = model.qtNodeIdForPw(100242); + REQUIRE(appQt != 0); + REQUIRE(sinkQt != 0); + REQUIRE(hwQt != 0); + + model.setNodePeakLevel(appQt, 0.0f); + model.setNodePeakLevel(sinkQt, 0.0f); + model.setNodePeakLevel(hwQt, 0.8f); + + REQUIRE(model.connectionPeakLevel(QtNodes::ConnectionId{sinkQt, 0u, hwQt, 0u}) == + Catch::Approx(0.0f)); + REQUIRE(model.connectionPeakLevel(QtNodes::ConnectionId{appQt, 0u, hwQt, 0u}) == + Catch::Approx(0.8f)); + + model.setNodePeakLevel(appQt, 0.4f); + REQUIRE(model.connectionPeakLevel(QtNodes::ConnectionId{appQt, 0u, hwQt, 0u}) == + Catch::Approx(0.4f)); +} + TEST_CASE("saveLayout stores and loadLayout restores view state") { auto tc = TestClient::Create(); if (!tc.available()) { SUCCEED("PipeWire unavailable"); return; }