"""The popup: a content-sized floating layer-shell panel anchored top-right, matching transmitter-panel/astro-menu's window.py idiom — dismissed with the launcher toggle, Esc, or the ✕ button, no click-outside-to-close. Anchored top-right (not centred) so it sits roughly under the volume control that invokes it — station-bar's volume badge and every eww variant's volume slider all live in the right-hand zone of their bar. This is a fixed approximation, not a live "appear under this exact widget" position: the wlr-layer-shell protocol only supports anchor+margin-to-screen-edge placement, not popup-relative-to-another-surface placement, and the panel is a separate process from whichever bar invoked it, so it has no way to learn that bar's precise pixel geometry. Tune RIGHT_MARGIN below if it drifts from whichever bar/build you're running against. Gtk.Window (layer TOP, anchored TOP+RIGHT, height/width = content) └ Gtk.Overlay main : .sb-panel (title, tab switcher, Gtk.Stack of the five tabs) over : hologram overlay over : ✕ close button (top-right) Owns the single AudioBackend and PeakMonitorManager shared by every tab — one `pactl subscribe` process and one live-metering manager for the whole panel, not one per tab. """ from __future__ import annotations import gi gi.require_version("Gtk", "4.0") gi.require_version("Gdk", "4.0") gi.require_version("Gtk4LayerShell", "1.0") from gi.repository import Gdk, Gtk # noqa: E402 from gi.repository import Gtk4LayerShell as LayerShell # noqa: E402 import config from backend.peaklevel import PeakMonitorManager from backend.pipewire import AudioBackend from lib.hologram import HologramOverlay from ui.applications_tab import ApplicationsTab from ui.cards_tab import CardsTab from ui.general_tab import GeneralTab from ui.input_tab import InputTab from ui.output_tab import OutputTab from ui.tabs import TabSwitcher PANEL_WIDTH = 460 TOP_MARGIN = 28 RIGHT_MARGIN = 160 # approximate horizontal offset to sit under the volume control class AudioPanelWindow(Gtk.ApplicationWindow): def __init__(self, app: Gtk.Application) -> None: super().__init__(application=app) self.set_name("audio-panel-window") self.add_css_class("sb-window") self.set_decorated(False) self._init_layer_shell() self._backend = AudioBackend(self._on_backend_changed) self._peaks = PeakMonitorManager() self.root = Gtk.Box(orientation=Gtk.Orientation.VERTICAL, spacing=10) self.root.set_name("panel-root") self.root.add_css_class("sb-panel") self.root.set_size_request(PANEL_WIDTH, -1) header = Gtk.CenterBox() header.add_css_class("sb-header") title = Gtk.Label(label="Audio Panel", xalign=0.0) title.add_css_class("sb-title") header.set_start_widget(title) self.root.append(header) self._stack = Gtk.Stack() self._stack.set_transition_type(Gtk.StackTransitionType.CROSSFADE) self._stack.set_transition_duration(150) self._applications_tab = ApplicationsTab(self._backend, self._peaks) self._output_tab = OutputTab(self._backend, self._peaks) self._input_tab = InputTab(self._backend, self._peaks) self._cards_tab = CardsTab(self._backend) self._general_tab = GeneralTab(self._backend, on_hologram_changed=self._on_hologram_setting_changed) self._tabs = { "applications": self._applications_tab, "output": self._output_tab, "input": self._input_tab, "cards": self._cards_tab, "general": self._general_tab, } tab_order = [ ("applications", "Applications", self._applications_tab), ("input", "Input Devices", self._input_tab), ("output", "Output Devices", self._output_tab), ("cards", "Cards", self._cards_tab), ("general", "General Settings", self._general_tab), ] for name, label, widget in tab_order: self._stack.add_titled(widget, name, label) self._active_tab_name = tab_order[0][0] self._tabswitcher = TabSwitcher(tab_order, self._stack, self._on_tab_changed) self.root.append(self._tabswitcher) self.root.append(self._stack) overlay = Gtk.Overlay() overlay.set_child(self.root) self._hologram = HologramOverlay(enabled=config.hologram_enabled(), fade_widget=self.root) overlay.add_overlay(self._hologram.widget) close = Gtk.Button(label="✕") close.add_css_class("close-btn") close.set_halign(Gtk.Align.END) close.set_valign(Gtk.Align.START) close.connect("clicked", lambda *_a: self.hide_panel()) overlay.add_overlay(close) self.set_child(overlay) key = Gtk.EventControllerKey() key.connect("key-pressed", self._on_key) self.add_controller(key) self._last_tick: float | None = None self._tick_id: int | None = None self.set_visible(False) # -- layer shell -------------------------------------------------------------- def _init_layer_shell(self) -> None: LayerShell.init_for_window(self) LayerShell.set_layer(self, LayerShell.Layer.TOP) LayerShell.set_namespace(self, "audio-panel") LayerShell.set_keyboard_mode(self, LayerShell.KeyboardMode.ON_DEMAND) LayerShell.set_anchor(self, LayerShell.Edge.TOP, True) LayerShell.set_anchor(self, LayerShell.Edge.RIGHT, True) LayerShell.set_margin(self, LayerShell.Edge.TOP, TOP_MARGIN) LayerShell.set_margin(self, LayerShell.Edge.RIGHT, RIGHT_MARGIN) # -- visibility ----------------------------------------------------------------- def show_panel(self) -> None: self.set_visible(True) self.present() if self._hologram.enabled and self._tick_id is None: self._tick_id = self.add_tick_callback(self._on_tick) self._hologram.start_intro() self._refresh_all() self._tabs[self._active_tab_name].activate() def hide_panel(self) -> None: self._tabs[self._active_tab_name].deactivate() self._peaks.stop_all() if self._hologram.enabled and self._tick_id is not None: self._hologram.start_outro(self._finish_hide) else: self._finish_hide() def _finish_hide(self) -> None: self.set_visible(False) if self._tick_id is not None: self.remove_tick_callback(self._tick_id) self._tick_id = None self._last_tick = None def toggle(self) -> None: if self.get_visible(): self.hide_panel() else: self.show_panel() # -- tabs --------------------------------------------------------------------- def _on_tab_changed(self, name: str) -> None: if name == self._active_tab_name: return self._tabs[self._active_tab_name].deactivate() self._active_tab_name = name self._tabs[name].activate() def _refresh_all(self) -> None: for tab in self._tabs.values(): tab.refresh() def _on_backend_changed(self) -> None: if not self.get_visible(): return self._refresh_all() def _on_hologram_setting_changed(self, enabled: bool) -> None: self._hologram.enabled = enabled if enabled and self.get_visible() and self._tick_id is None: self._tick_id = self.add_tick_callback(self._on_tick) # -- hologram tick -------------------------------------------------------------- def _on_tick(self, _widget, frame_clock) -> bool: now = frame_clock.get_frame_time() / 1_000_000 dt = 0.0 if self._last_tick is None else max(0.0, now - self._last_tick) self._last_tick = now self._hologram.tick(dt) return True def _on_key(self, _c, keyval, _kc, _state) -> bool: if keyval == Gdk.KEY_Escape: self.hide_panel() return True return False