Merge branch 'blueprints' into 'petgraph'

Replace standard ui xml with Blueprints

See merge request pipewire/helvum!82
This commit is contained in:
Naveen Prashanth 2026-05-08 15:18:58 +00:00
commit ae559d0965
19 changed files with 243 additions and 283 deletions

5
.gitignore vendored
View file

@ -3,6 +3,7 @@
/.vscode
/_build
/target
/*.Dockerfile
**/*.Dockerfile
/docker-compose.*.yml
*.sh
*.sh
**/*.ui

55
build.rs Normal file
View file

@ -0,0 +1,55 @@
fn main() {
let blp_files = [
"src/ui/window.blp",
"src/ui/graph/node.blp",
"src/ui/graph/port.blp",
"src/ui/graph/zoomentry.blp",
];
println!("cargo:warning=Helvum build script starting...");
let root = std::env::var("CARGO_MANIFEST_DIR").unwrap();
let out_dir = std::env::var("OUT_DIR").unwrap();
for blp in blp_files {
let blp_abs = std::path::Path::new(&root).join(blp);
let ui_abs = blp_abs.with_extension("ui");
println!("cargo:rerun-if-changed={}", blp_abs.display());
let output = std::process::Command::new("blueprint-compiler")
.arg("compile")
.arg("--output")
.arg(&ui_abs)
.arg(&blp_abs)
.output()
.expect("Failed to run blueprint-compiler");
if !output.status.success() {
panic!(
"Failed to compile blueprint {}: {}",
blp,
String::from_utf8_lossy(&output.stderr)
);
}
}
let gresource_xml = std::path::Path::new(&root).join("src/ui/helvum.gresource.xml");
println!("cargo:rerun-if-changed={}", gresource_xml.display());
let output = std::process::Command::new("glib-compile-resources")
.arg("--target")
.arg(std::path::Path::new(&out_dir).join("helvum.gresource"))
.arg("--sourcedir")
.arg(std::path::Path::new(&root).join("src/ui"))
.arg(&gresource_xml)
.output()
.expect("Failed to run glib-compile-resources");
if !output.status.success() {
panic!(
"Failed to compile gresource: {}",
String::from_utf8_lossy(&output.stderr)
);
}
}

View file

@ -143,6 +143,10 @@ fn main() -> Result<(), Box<dyn std::error::Error>> {
init_glib_logger();
gtk::init()?;
let res_bytes = include_bytes!(concat!(env!("OUT_DIR"), "/helvum.gresource"));
let resource = adw::gio::Resource::from_data(&adw::glib::Bytes::from_static(res_bytes))?;
adw::gio::resources_register(&resource);
// Aquire main context so that we can attach the gtk channel later.
let ctx = glib::MainContext::default();
let _guard = ctx.acquire().unwrap();

View file

@ -20,6 +20,11 @@
@define-color media-type-midi rgb(200, 0, 50);
@define-color media-type-unknown rgb(128, 128, 128);
.port-handle {
border-radius: 50%;
background-color: @media-type-unknown;
}
.audio {
background: @media-type-audio;
color: black;
@ -36,8 +41,6 @@
}
node {
/* Compared to the default card color, this is not transparent in dark-mode
and provides a better contrast to the background in light mode */
background-color: @headerbar_bg_color;
}
@ -49,10 +52,6 @@ port label {
padding: 4px 6px;
}
port-handle {
border-radius: 50%;
background-color: @media-type-unknown;
}
button.rounded {
padding: 6px;

View file

@ -20,8 +20,6 @@ mod node;
pub use node::*;
mod port;
pub use port::*;
mod port_handle;
pub use port_handle::*;
mod link;
pub use link::*;
mod zoomentry;

39
src/ui/graph/node.blp Normal file
View file

@ -0,0 +1,39 @@
using Gtk 4.0;
template HelvumNode : Widget {
styles ["card"]
Gtk.Box {
orientation: vertical;
Gtk.Box {
styles ["node-title"]
orientation: vertical;
spacing: 1;
Gtk.Label node_name {
styles ["heading"]
wrap: true;
ellipsize: end;
lines: 2;
max-width-chars: 20;
}
Gtk.Label media_name {
styles ["dim-label", "caption"]
visible: false;
wrap: true;
ellipsize: end;
lines: 2;
max-width-chars: 20;
}
}
Gtk.Separator separator {
/* The node will show the seperator only once ports are added to it */
visible: false;
}
Gtk.Grid port_grid {}
}
}

View file

@ -30,7 +30,7 @@ mod imp {
#[derive(glib::Properties, gtk::CompositeTemplate, Default)]
#[properties(wrapper_type = super::Node)]
#[template(file = "node.ui")]
#[template(resource = "/org/pipewire/Helvum/graph/node.ui")]
pub struct Node {
#[property(get, set, construct_only)]
pub(super) pipewire_id: Cell<u32>,

View file

@ -1,56 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<interface>
<requires lib="gtk" version="4.0"/>
<template class="HelvumNode" parent="GtkWidget">
<style>
<class name="card"></class>
</style>>
<child>
<object class="GtkBox">
<property name="orientation">vertical</property>
<child>
<object class="GtkBox">
<style>
<class name="node-title"></class>
</style>
<property name="orientation">vertical</property>
<property name="spacing">1</property>
<child>
<object class="GtkLabel" id="node_name">
<style>
<class name="heading"></class>
</style>
<property name="wrap">true</property>
<property name="ellipsize">PANGO_ELLIPSIZE_END</property>
<property name="lines">2</property>
<property name="max-width-chars">20</property>
</object>
</child>
<child>
<object class="GtkLabel" id="media_name">
<style>
<class name="dim-label"></class>
<class name="caption"></class>
</style>
<property name="visible">false</property>
<property name="wrap">true</property>
<property name="ellipsize">PANGO_ELLIPSIZE_END</property>
<property name="lines">2</property>
<property name="max-width-chars">20</property>
</object>
</child>
</object>
</child>
<child>
<object class="GtkSeparator" id="separator">
<!-- The node will show the seperator only once ports are added to it -->
<property name="visible">false</property>
</object>
</child>
<child>
<object class="GtkGrid" id="port_grid"></object>
</child>
</object>
</child>
</template>
</interface>

17
src/ui/graph/port.blp Normal file
View file

@ -0,0 +1,17 @@
using Gtk 4.0;
template HelvumPort : Widget {
hexpand: true;
Gtk.Label label {
wrap: true;
ellipsize: end;
lines: 2;
max-width-chars: 20;
}
Gtk.Box handle {
halign: center;
valign: center;
}
}

View file

@ -25,8 +25,6 @@ use adw::{
pub use imp::{PortDirection, PortMediaType};
use libspa::{param::format::MediaType, utils::Direction};
use super::PortHandle;
mod imp {
use super::*;
@ -99,7 +97,7 @@ mod imp {
/// Graphical representation of a pipewire port.
#[derive(gtk::CompositeTemplate)]
#[template(file = "port.ui")]
#[template(resource = "/org/pipewire/Helvum/graph/port.ui")]
pub struct Port {
pub(super) pipewire_id: Cell<u32>,
pub(super) media_type: Cell<PortMediaType>,
@ -107,7 +105,7 @@ mod imp {
#[template_child]
pub(super) label: TemplateChild<gtk::Label>,
#[template_child]
pub(super) handle: TemplateChild<PortHandle>,
pub(super) handle: TemplateChild<gtk::Box>,
}
impl Default for Port {
@ -143,6 +141,9 @@ mod imp {
fn constructed(&self) {
self.parent_constructed();
self.handle.add_css_class("port-handle");
self.handle.set_size_request(14, 14);
// Force left-to-right direction for the ports grid to avoid messed up UI when defaulting to right-to-left
gtk::prelude::WidgetExt::set_direction(&*self.obj(), gtk::TextDirection::Ltr);

View file

@ -1,19 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<interface>
<requires lib="gtk" version="4.0"/>
<template class="HelvumPort" parent="GtkWidget">
<property name="hexpand">true</property>
<child>
<object class="GtkLabel" id="label">
<property name="wrap">true</property>
<property name="ellipsize">PANGO_ELLIPSIZE_END</property>
<property name="lines">2</property>
<property name="max-width-chars">20</property>
</object>
</child>
<child>
<object class="HelvumPortHandle" id="handle"></object>
</child>
</template>
</interface>

View file

@ -1,84 +0,0 @@
// Copyright 2021 Tom A. Wagner <tom.a.wagner@protonmail.com>
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License version 3 as published by
// the Free Software Foundation.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with this program. If not, see <http://www.gnu.org/licenses/>.
//
// SPDX-License-Identifier: GPL-3.0-only
use adw::{glib, gtk, prelude::*, subclass::prelude::*};
mod imp {
use super::*;
#[derive(Default)]
pub struct PortHandle {}
#[glib::object_subclass]
impl ObjectSubclass for PortHandle {
const NAME: &'static str = "HelvumPortHandle";
type Type = super::PortHandle;
type ParentType = gtk::Widget;
fn class_init(klass: &mut Self::Class) {
klass.set_css_name("port-handle");
}
}
impl ObjectImpl for PortHandle {
fn constructed(&self) {
self.parent_constructed();
let obj = &*self.obj();
obj.set_halign(gtk::Align::Center);
obj.set_valign(gtk::Align::Center);
}
}
impl WidgetImpl for PortHandle {
fn request_mode(&self) -> gtk::SizeRequestMode {
gtk::SizeRequestMode::ConstantSize
}
fn measure(&self, _orientation: gtk::Orientation, _for_size: i32) -> (i32, i32, i32, i32) {
(Self::HANDLE_SIZE, Self::HANDLE_SIZE, -1, -1)
}
}
impl PortHandle {
pub const HANDLE_SIZE: i32 = 14;
}
}
glib::wrapper! {
pub struct PortHandle(ObjectSubclass<imp::PortHandle>)
@extends gtk::Widget, @implements gtk::Accessible, gtk::Buildable, gtk::ConstraintTarget;
}
impl PortHandle {
pub fn new() -> Self {
glib::Object::new()
}
pub fn get_link_anchor(&self) -> gtk::graphene::Point {
gtk::graphene::Point::new(
imp::PortHandle::HANDLE_SIZE as f32 / 2.0,
imp::PortHandle::HANDLE_SIZE as f32 / 2.0,
)
}
}
impl Default for PortHandle {
fn default() -> Self {
Self::new()
}
}

View file

@ -0,0 +1,24 @@
using Gtk 4.0;
template HelvumZoomEntry : Box {
spacing: 12;
Gtk.Entry entry {
secondary-icon-name: "go-down-symbolic";
input-purpose: digits;
max-width-chars: 5;
styles ["osd", "rounded"]
}
Gtk.Button zoom_out_button {
icon-name: "zoom-out-symbolic";
tooltip-text: "Zoom out";
styles ["osd", "rounded"]
}
Gtk.Button zoom_in_button {
icon-name: "zoom-in-symbolic";
tooltip-text: "Zoom in";
styles ["osd", "rounded"]
}
}

View file

@ -11,7 +11,7 @@ mod imp {
use std::sync::LazyLock;
#[derive(gtk::CompositeTemplate)]
#[template(file = "zoomentry.ui")]
#[template(resource = "/org/pipewire/Helvum/graph/zoomentry.ui")]
pub struct ZoomEntry {
pub graphview: RefCell<Option<GraphView>>,
#[template_child]

View file

@ -1,37 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<interface>
<template class="HelvumZoomEntry" parent="GtkBox">
<property name="spacing">12</property>
<child>
<object class="GtkEntry" id="entry">
<property name="secondary-icon-name">go-down-symbolic</property>
<property name="input-purpose">digits</property>
<property name="max-width-chars">5</property>
<style>
<class name="osd"/>
<class name="rounded"/>
</style>
</object>
</child>
<child>
<object class="GtkButton" id="zoom_out_button">
<property name="icon-name">zoom-out-symbolic</property>
<property name="tooltip-text">Zoom out</property>
<style>
<class name="osd"/>
<class name="rounded"/>
</style>
</object>
</child>
<child>
<object class="GtkButton" id="zoom_in_button">
<property name="icon-name">zoom-in-symbolic</property>
<property name="tooltip-text">Zoom in</property>
<style>
<class name="osd"/>
<class name="rounded"/>
</style>
</object>
</child>
</template>
</interface>

View file

@ -0,0 +1,9 @@
<?xml version="1.0" encoding="UTF-8"?>
<gresources>
<gresource prefix="/org/pipewire/Helvum">
<file preprocess="xml-stripblanks">window.ui</file>
<file preprocess="xml-stripblanks">graph/node.ui</file>
<file preprocess="xml-stripblanks">graph/port.ui</file>
<file preprocess="xml-stripblanks">graph/zoomentry.ui</file>
</gresource>
</gresources>

39
src/ui/window.blp Normal file
View file

@ -0,0 +1,39 @@
using Gtk 4.0;
using Adw 1;
menu primary_menu {
item ("_About Helvum", "app.about")
}
template HelvumWindow : Adw.ApplicationWindow {
default-width: 1280;
default-height: 720;
title: "Helvum - Pipewire Patchbay";
Adw.ToolbarView {
[top]
Adw.HeaderBar header_bar {
[end]
Gtk.MenuButton {
icon-name: "open-menu-symbolic";
menu-model: primary_menu;
}
}
content: Gtk.Box {
orientation: vertical;
Adw.Banner connection_banner {
title: _("Disconnected");
revealed: false;
}
Gtk.Overlay overlay {
Gtk.ScrolledWindow scrolled_window {
hexpand: true;
vexpand: true;
}
}
};
}
}

View file

@ -5,9 +5,9 @@ use super::graph;
mod imp {
use super::*;
#[derive(Default, gtk::CompositeTemplate, glib::Properties)]
#[derive(gtk::CompositeTemplate, glib::Properties)]
#[properties(wrapper_type = super::Window)]
#[template(file = "window.ui")]
#[template(resource = "/org/pipewire/Helvum/window.ui")]
pub struct Window {
#[template_child]
pub header_bar: TemplateChild<adw::HeaderBar>,
@ -15,8 +15,30 @@ mod imp {
#[property(type = adw::Banner, get = |_| self.connection_banner.clone())]
pub connection_banner: TemplateChild<adw::Banner>,
#[template_child]
#[property(type = graph::GraphView, get = |_| self.graph.clone())]
pub graph: TemplateChild<graph::GraphView>,
pub overlay: TemplateChild<gtk::Overlay>,
#[template_child]
pub scrolled_window: TemplateChild<gtk::ScrolledWindow>,
#[property(type = graph::GraphView, get = |this: &Self| this.graph.clone())]
pub graph: graph::GraphView,
pub zoom_entry: graph::ZoomEntry,
}
impl Default for Window {
fn default() -> Self {
let graph = graph::GraphView::new();
// We'll set the zoomed widget later in constructed
let zoom_entry = glib::Object::new::<graph::ZoomEntry>();
Self {
header_bar: TemplateChild::default(),
connection_banner: TemplateChild::default(),
overlay: TemplateChild::default(),
scrolled_window: TemplateChild::default(),
graph,
zoom_entry,
}
}
}
#[glib::object_subclass]
@ -39,7 +61,21 @@ mod imp {
}
#[glib::derived_properties]
impl ObjectImpl for Window {}
impl ObjectImpl for Window {
fn constructed(&self) {
self.parent_constructed();
self.scrolled_window.set_child(Some(&self.graph));
self.zoom_entry.set_halign(gtk::Align::End);
self.zoom_entry.set_valign(gtk::Align::End);
self.zoom_entry.set_margin_end(24);
self.zoom_entry.set_margin_bottom(24);
self.zoom_entry.set_property("zoomed-widget", &self.graph);
self.overlay.add_overlay(&self.zoom_entry);
}
}
impl WidgetImpl for Window {}
impl WindowImpl for Window {}
impl ApplicationWindowImpl for Window {}

View file

@ -1,66 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<interface>
<requires lib="gtk" version="4.0"/>
<requires lib="Adw" version="1.4"/>
<menu id="primary_menu">
<section>
<item>
<attribute name="label">_About Helvum</attribute>
<attribute name="action">app.about</attribute>
</item>
</section>
</menu>
<template class="HelvumWindow" parent="AdwApplicationWindow">
<property name="default-width">1280</property>
<property name="default-height">720</property>
<property name="title">Helvum - Pipewire Patchbay</property>
<child>
<object class="AdwToolbarView">
<child type="top">
<object class="AdwHeaderBar" id="header_bar">
<child type="end">
<object class="GtkMenuButton">
<property name="icon-name">open-menu-symbolic</property>
<property name="menu-model">primary_menu</property>
</object>
</child>
</object>
</child>
<property name="content">
<object class="GtkBox">
<property name="orientation">vertical</property>
<child>
<object class="AdwBanner" id="connection_banner">
<property name="title" translatable="yes">Disconnected</property>
<property name="revealed">false</property>
</object>
</child>
<child>
<object class="GtkOverlay">
<child>
<object class="GtkScrolledWindow">
<child>
<object class="HelvumGraphView" id="graph">
<property name="hexpand">true</property>
<property name="vexpand">true</property>
</object>
</child>
</object>
</child>
<child type="overlay">
<object class="HelvumZoomEntry">
<property name="zoomed-widget">graph</property>
<property name="halign">end</property>
<property name="valign">end</property>
<property name="margin-end">24</property>
<property name="margin-bottom">24</property>
</object>
</child>
</object>
</child>
</object>
</property>
</object>
</child>
</template>
</interface>