From 8f341f631a12c0203ff67d376e02a1a0171808c6 Mon Sep 17 00:00:00 2001 From: Joey Yakimowich-Payne Date: Thu, 29 Jan 2026 22:17:33 -0700 Subject: [PATCH] GUI Milestone 2 --- GUI_PLAN.md | 42 +++++------ gui/WarpGraphModel.cpp | 157 ++++++++++++++++++++++++++++++++++++++++- gui/WarpGraphModel.h | 13 ++++ 3 files changed, 190 insertions(+), 22 deletions(-) diff --git a/GUI_PLAN.md b/GUI_PLAN.md index cb8daaf..99a9d46 100644 --- a/GUI_PLAN.md +++ b/GUI_PLAN.md @@ -35,27 +35,27 @@ A Qt6-based node editor GUI for warppipe using the QtNodes (nodeeditor) library. - [x] Use port name from PortInfo as port label - [x] Verify nodes appear in graph view with correct titles and ports -- [ ] Milestone 2 - Visual Styling and Node Types - - [ ] Define node type classification based on `media_class`: - - [ ] Sink → "Hardware Sink" (blue-gray base color) - - [ ] Source → "Hardware Source" (blue-gray base color) - - [ ] Virtual sinks created by warppipe → "Virtual Sink" (green base color) - - [ ] Virtual sources created by warppipe → "Virtual Source" (green base color) - - [ ] Application audio streams (ephemeral) → "Application" (brown/orange base color) - - [ ] Implement custom NodeStyle via `nodeData(NodeRole::Style)`: - - [ ] Return QtNodes::NodeStyle::toJson().toVariantMap() - - [ ] Set GradientColor0-3, NormalBoundaryColor, FontColor based on node type - - [ ] Reference potato's `nodeStyleVariant()` function for color scheme - - [ ] Detect ephemeral (application) nodes: - - [ ] Track node appearance/disappearance via Client poll or registry events - - [ ] Mark node as "inactive" if it disappears (no audio playing) - - [ ] Persist inactive nodes in graph model (do NOT remove from visual graph) - - [ ] Apply "ghost" styling to inactive nodes: - - [ ] Set `Opacity = 0.6f` (vs 1.0f for active) - - [ ] Darken gradient colors (use `.darker(150-180)`) - - [ ] Fade font color (lighter gray) - - [ ] Keep connections visible with faded style - - [ ] Verify: Application nodes appear vibrant when active, fade when inactive, never disappear +- [x] Milestone 2 - Visual Styling and Node Types + - [x] Define node type classification based on `media_class`: + - [x] Sink → "Hardware Sink" (blue-gray base color) + - [x] Source → "Hardware Source" (blue-gray base color) + - [x] Virtual sinks created by warppipe → "Virtual Sink" (green base color) + - [x] Virtual sources created by warppipe → "Virtual Source" (green base color) + - [x] Application audio streams (ephemeral) → "Application" (brown/orange base color) + - [x] Implement custom NodeStyle via `nodeData(NodeRole::Style)`: + - [x] Return QtNodes::NodeStyle::toJson().toVariantMap() + - [x] Set GradientColor0-3, NormalBoundaryColor, FontColor based on node type + - [x] Reference potato's `nodeStyleVariant()` function for color scheme + - [x] Detect ephemeral (application) nodes: + - [x] Track node appearance/disappearance via Client poll or registry events + - [x] Mark node as "inactive" if it disappears (no audio playing) + - [x] Persist inactive nodes in graph model (do NOT remove from visual graph) + - [x] Apply "ghost" styling to inactive nodes: + - [x] Set `Opacity = 0.6f` (vs 1.0f for active) + - [x] Darken gradient colors (use `.darker(150-180)`) + - [x] Fade font color (lighter gray) + - [x] Keep connections visible with faded style + - [x] Verify: Application nodes appear vibrant when active, fade when inactive, never disappear - [ ] Milestone 3 - Link Visualization and Drag-Connect - [ ] Implement connection mapping: diff --git a/gui/WarpGraphModel.cpp b/gui/WarpGraphModel.cpp index c85895f..e976502 100644 --- a/gui/WarpGraphModel.cpp +++ b/gui/WarpGraphModel.cpp @@ -1,8 +1,12 @@ #include "WarpGraphModel.h" +#include #include #include +#include +#include + #include #include @@ -141,6 +145,11 @@ QVariant WarpGraphModel::nodeData(QtNodes::NodeId nodeId, return static_cast(data.outputPorts.size()); case QtNodes::NodeRole::Type: return QString("PipeWire"); + case QtNodes::NodeRole::Style: { + bool ghost = m_ghostNodes.find(nodeId) != m_ghostNodes.end(); + WarpNodeType type = classifyNode(data.info); + return styleForNode(type, ghost); + } default: return QVariant(); } @@ -275,8 +284,50 @@ void WarpGraphModel::refreshFromClient() { auto existing = m_pwToQt.find(nodeInfo.id.value); if (existing != m_pwToQt.end()) { - auto &data = m_nodes[existing->second]; + QtNodes::NodeId qtId = existing->second; + auto &data = m_nodes[qtId]; data.info = nodeInfo; + if (m_ghostNodes.erase(qtId)) { + Q_EMIT nodeUpdated(qtId); + } + continue; + } + + QtNodes::NodeId ghostMatch = 0; + std::string nodeName = nodeInfo.name; + for (const auto &ghostId : m_ghostNodes) { + auto ghostIt = m_nodes.find(ghostId); + if (ghostIt != m_nodes.end() && + ghostIt->second.info.name == nodeName) { + ghostMatch = ghostId; + break; + } + } + + if (ghostMatch != 0) { + m_ghostNodes.erase(ghostMatch); + m_pwToQt.emplace(nodeInfo.id.value, ghostMatch); + auto &data = m_nodes[ghostMatch]; + data.info = nodeInfo; + + auto portsResult = m_client->ListPorts(nodeInfo.id); + if (portsResult.ok()) { + data.inputPorts.clear(); + data.outputPorts.clear(); + for (const auto &port : portsResult.value) { + if (port.is_input) { + data.inputPorts.push_back(port); + } else { + data.outputPorts.push_back(port); + } + } + std::sort(data.inputPorts.begin(), data.inputPorts.end(), + [](const auto &a, const auto &b) { return a.name < b.name; }); + std::sort(data.outputPorts.begin(), data.outputPorts.end(), + [](const auto &a, const auto &b) { return a.name < b.name; }); + } + + Q_EMIT nodeUpdated(ghostMatch); continue; } @@ -310,6 +361,33 @@ void WarpGraphModel::refreshFromClient() { Q_EMIT nodeCreated(qtId); } + std::vector disappearedPwIds; + for (const auto &[pwId, qtId] : m_pwToQt) { + if (seenPwIds.find(pwId) == seenPwIds.end()) { + disappearedPwIds.push_back(pwId); + } + } + for (uint32_t pwId : disappearedPwIds) { + auto it = m_pwToQt.find(pwId); + if (it == m_pwToQt.end()) { + continue; + } + QtNodes::NodeId qtId = it->second; + auto nodeIt = m_nodes.find(qtId); + if (nodeIt == m_nodes.end()) { + continue; + } + WarpNodeType type = classifyNode(nodeIt->second.info); + if (type == WarpNodeType::kApplication) { + m_ghostNodes.insert(qtId); + m_pwToQt.erase(it); + Q_EMIT nodeUpdated(qtId); + } else { + m_pwToQt.erase(it); + deleteNode(qtId); + } + } + auto linksResult = m_client->ListLinks(); if (linksResult.ok()) { std::unordered_set seenLinkIds; @@ -407,3 +485,80 @@ QPointF WarpGraphModel::nextPosition() const { double y = (count / 4) * 200.0; return QPointF(x, y); } + +bool WarpGraphModel::isGhost(QtNodes::NodeId nodeId) const { + return m_ghostNodes.find(nodeId) != m_ghostNodes.end(); +} + +WarpNodeType +WarpGraphModel::classifyNode(const warppipe::NodeInfo &info) { + const std::string &mc = info.media_class; + + bool isVirtual = (info.name.find("warppipe") != std::string::npos); + + if (mc == "Audio/Sink" || mc == "Audio/Duplex") { + return isVirtual ? WarpNodeType::kVirtualSink : WarpNodeType::kHardwareSink; + } + if (mc == "Audio/Source") { + return isVirtual ? WarpNodeType::kVirtualSource + : WarpNodeType::kHardwareSource; + } + if (mc == "Stream/Output/Audio" || mc == "Stream/Input/Audio") { + return WarpNodeType::kApplication; + } + + return WarpNodeType::kUnknown; +} + +QVariant WarpGraphModel::styleForNode(WarpNodeType type, bool ghost) { + QtNodes::NodeStyle style = QtNodes::StyleCollection::nodeStyle(); + + QColor base; + switch (type) { + case WarpNodeType::kHardwareSink: + case WarpNodeType::kHardwareSource: + base = QColor(72, 94, 118); + break; + case WarpNodeType::kVirtualSink: + case WarpNodeType::kVirtualSource: + base = QColor(62, 122, 104); + break; + case WarpNodeType::kApplication: + base = QColor(138, 104, 72); + break; + default: + base = QColor(86, 94, 108); + break; + } + + if (ghost) { + style.GradientColor0 = base.darker(150); + style.GradientColor1 = base.darker(160); + style.GradientColor2 = base.darker(170); + style.GradientColor3 = base.darker(180); + style.NormalBoundaryColor = base.darker(130); + style.FontColor = QColor(160, 168, 182); + style.FontColorFaded = QColor(120, 128, 142); + style.ConnectionPointColor = QColor(140, 148, 160); + style.FilledConnectionPointColor = QColor(180, 140, 80); + style.Opacity = 0.6f; + } else { + style.GradientColor0 = base.lighter(120); + style.GradientColor1 = base.lighter(108); + style.GradientColor2 = base.darker(105); + style.GradientColor3 = base.darker(120); + style.NormalBoundaryColor = base.lighter(135); + style.FontColor = QColor(236, 240, 246); + style.FontColorFaded = QColor(160, 168, 182); + style.ConnectionPointColor = QColor(200, 208, 220); + style.FilledConnectionPointColor = QColor(255, 165, 0); + style.Opacity = 1.0f; + } + + style.SelectedBoundaryColor = QColor(255, 165, 0); + style.PenWidth = 1.3f; + style.HoveredPenWidth = 2.4f; + style.ConnectionPointDiameter = 10.0f; + + return style.toJson().toVariantMap(); +} diff --git a/gui/WarpGraphModel.h b/gui/WarpGraphModel.h index f56119a..4615f45 100644 --- a/gui/WarpGraphModel.h +++ b/gui/WarpGraphModel.h @@ -13,6 +13,15 @@ #include #include +enum class WarpNodeType : uint8_t { + kUnknown = 0, + kHardwareSink, + kHardwareSource, + kVirtualSink, + kVirtualSource, + kApplication, +}; + struct WarpNodeData { warppipe::NodeInfo info; std::vector inputPorts; @@ -54,9 +63,12 @@ public: void refreshFromClient(); const WarpNodeData *warpNodeData(QtNodes::NodeId nodeId) const; QtNodes::NodeId qtNodeIdForPw(uint32_t pwNodeId) const; + bool isGhost(QtNodes::NodeId nodeId) const; private: static QString captionForNode(const warppipe::NodeInfo &info); + static WarpNodeType classifyNode(const warppipe::NodeInfo &info); + static QVariant styleForNode(WarpNodeType type, bool ghost); QPointF nextPosition() const; warppipe::Client *m_client = nullptr; @@ -70,4 +82,5 @@ private: std::unordered_map m_positions; std::unordered_map m_sizes; + std::unordered_set m_ghostNodes; };