diff --git a/.gitignore b/.gitignore index c9f45b9..e227f68 100644 --- a/.gitignore +++ b/.gitignore @@ -3,6 +3,7 @@ /.vscode /_build /target -/*.Dockerfile +**/*.Dockerfile /docker-compose.*.yml -*.sh \ No newline at end of file +*.sh +**/*.ui \ No newline at end of file diff --git a/build.rs b/build.rs new file mode 100644 index 0000000..d4466e3 --- /dev/null +++ b/build.rs @@ -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) + ); + } +} diff --git a/src/main.rs b/src/main.rs index 6f58fa1..b07a269 100644 --- a/src/main.rs +++ b/src/main.rs @@ -143,6 +143,10 @@ fn main() -> Result<(), Box> { 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(); diff --git a/src/style.css b/src/style.css index 397fedb..7f1fde9 100644 --- a/src/style.css +++ b/src/style.css @@ -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; diff --git a/src/ui/graph/mod.rs b/src/ui/graph/mod.rs index 36f36e2..691b9ff 100644 --- a/src/ui/graph/mod.rs +++ b/src/ui/graph/mod.rs @@ -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; diff --git a/src/ui/graph/node.blp b/src/ui/graph/node.blp new file mode 100644 index 0000000..6b18af2 --- /dev/null +++ b/src/ui/graph/node.blp @@ -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 {} + } +} diff --git a/src/ui/graph/node.rs b/src/ui/graph/node.rs index 5fd1b8a..80f8970 100644 --- a/src/ui/graph/node.rs +++ b/src/ui/graph/node.rs @@ -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, diff --git a/src/ui/graph/node.ui b/src/ui/graph/node.ui deleted file mode 100644 index 9a80c56..0000000 --- a/src/ui/graph/node.ui +++ /dev/null @@ -1,56 +0,0 @@ - - - - - diff --git a/src/ui/graph/port.blp b/src/ui/graph/port.blp new file mode 100644 index 0000000..c57c79f --- /dev/null +++ b/src/ui/graph/port.blp @@ -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; + } +} diff --git a/src/ui/graph/port.rs b/src/ui/graph/port.rs index 754757e..c4b8155 100644 --- a/src/ui/graph/port.rs +++ b/src/ui/graph/port.rs @@ -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, pub(super) media_type: Cell, @@ -107,7 +105,7 @@ mod imp { #[template_child] pub(super) label: TemplateChild, #[template_child] - pub(super) handle: TemplateChild, + pub(super) handle: TemplateChild, } 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); diff --git a/src/ui/graph/port.ui b/src/ui/graph/port.ui deleted file mode 100644 index 13722ca..0000000 --- a/src/ui/graph/port.ui +++ /dev/null @@ -1,19 +0,0 @@ - - - - - - diff --git a/src/ui/graph/port_handle.rs b/src/ui/graph/port_handle.rs deleted file mode 100644 index 7eb3a9a..0000000 --- a/src/ui/graph/port_handle.rs +++ /dev/null @@ -1,84 +0,0 @@ -// Copyright 2021 Tom A. Wagner -// -// 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 . -// -// 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) - @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() - } -} diff --git a/src/ui/graph/zoomentry.blp b/src/ui/graph/zoomentry.blp new file mode 100644 index 0000000..4b7ad93 --- /dev/null +++ b/src/ui/graph/zoomentry.blp @@ -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"] + } +} diff --git a/src/ui/graph/zoomentry.rs b/src/ui/graph/zoomentry.rs index a53697b..1b37d3a 100644 --- a/src/ui/graph/zoomentry.rs +++ b/src/ui/graph/zoomentry.rs @@ -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>, #[template_child] diff --git a/src/ui/graph/zoomentry.ui b/src/ui/graph/zoomentry.ui deleted file mode 100644 index def1a6a..0000000 --- a/src/ui/graph/zoomentry.ui +++ /dev/null @@ -1,37 +0,0 @@ - - - - diff --git a/src/ui/helvum.gresource.xml b/src/ui/helvum.gresource.xml new file mode 100644 index 0000000..3b5c14e --- /dev/null +++ b/src/ui/helvum.gresource.xml @@ -0,0 +1,9 @@ + + + + window.ui + graph/node.ui + graph/port.ui + graph/zoomentry.ui + + diff --git a/src/ui/window.blp b/src/ui/window.blp new file mode 100644 index 0000000..ee549d4 --- /dev/null +++ b/src/ui/window.blp @@ -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; + } + } + }; + } +} diff --git a/src/ui/window.rs b/src/ui/window.rs index 1f321d0..6606815 100644 --- a/src/ui/window.rs +++ b/src/ui/window.rs @@ -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, @@ -15,8 +15,30 @@ mod imp { #[property(type = adw::Banner, get = |_| self.connection_banner.clone())] pub connection_banner: TemplateChild, #[template_child] - #[property(type = graph::GraphView, get = |_| self.graph.clone())] - pub graph: TemplateChild, + pub overlay: TemplateChild, + #[template_child] + pub scrolled_window: TemplateChild, + + #[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::(); + + 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 {} diff --git a/src/ui/window.ui b/src/ui/window.ui deleted file mode 100644 index c1df524..0000000 --- a/src/ui/window.ui +++ /dev/null @@ -1,66 +0,0 @@ - - - - - -
- - _About Helvum - app.about - -
-
- -