From 3c7cf117a3c76736150dee5584301daf9c13cba9 Mon Sep 17 00:00:00 2001 From: VU3ESV Date: Thu, 4 Jun 2026 21:23:39 +0200 Subject: [PATCH] fix(ui): fill the host pane and show controls when hosted in the Suite MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Two issues when LP-700 runs out-of-process inside the Amateur Radio Suite: 1. The UI didn't fill the host pane — ContentView used only minWidth/minHeight and meterPane used .frame(maxWidth: .infinity, alignment: .top) with no maxHeight, so it floated undersized. Add maxWidth/maxHeight .infinity so it fills and top-anchors. 2. The toolbar controls (connection badge, backend badge, connection + SETUP buttons) were invisible — SwiftUI `.toolbar` items render in the window title bar, which a hosted .appex doesn't have. Add an `embedded` flag: the extension passes embedded: true and ContentView renders the same controls inline (embeddedToolbar); the standalone app keeps its window toolbar. Co-Authored-By: Claude Opus 4.8 --- Sources/LP700App/LP700Extension.swift | 5 ++- Sources/LP700App/Views/ContentView.swift | 57 ++++++++++++++++++++---- 2 files changed, 52 insertions(+), 10 deletions(-) diff --git a/Sources/LP700App/LP700Extension.swift b/Sources/LP700App/LP700Extension.swift index e50d724..21a7dbb 100644 --- a/Sources/LP700App/LP700Extension.swift +++ b/Sources/LP700App/LP700Extension.swift @@ -12,6 +12,9 @@ public enum LP700Extension { @MainActor public static func rootView(defaults: UserDefaults? = nil) -> AnyView { if let defaults { AppDefaults.store = defaults } - return AnyView(ContentView(vm: MeterViewModel())) + // `embedded: true` renders the toolbar controls inline. A hosted .appex has no window + // title bar, so SwiftUI `.toolbar` items never appear; the standalone app (embedded: + // false) keeps its window toolbar. + return AnyView(ContentView(vm: MeterViewModel(), embedded: true)) } } diff --git a/Sources/LP700App/Views/ContentView.swift b/Sources/LP700App/Views/ContentView.swift index e70caf3..6de930d 100644 --- a/Sources/LP700App/Views/ContentView.swift +++ b/Sources/LP700App/Views/ContentView.swift @@ -4,25 +4,64 @@ import AppKit struct ContentView: View { @ObservedObject var vm: MeterViewModel @AppStorage("serverURL", store: AppDefaults.store) private var persistedURL: String = "" + /// When hosted out-of-process in the Suite there is no window title bar, so `.toolbar` + /// items never appear. In that case render the same controls inline (see `embeddedToolbar`). + var embedded: Bool = false var body: some View { - Group { - if vm.connectionSheetOpen || (!vm.hasConfiguredServer && persistedURL.isEmpty) { - ConnectionPlaceholder(vm: vm) - .frame(maxWidth: .infinity, maxHeight: .infinity) - } else { - meterPane + VStack(spacing: 0) { + if embedded { embeddedToolbar } + Group { + if vm.connectionSheetOpen || (!vm.hasConfiguredServer && persistedURL.isEmpty) { + ConnectionPlaceholder(vm: vm) + .frame(maxWidth: .infinity, maxHeight: .infinity) + } else { + meterPane + } } + .frame(maxWidth: .infinity, maxHeight: .infinity) } .navigationTitle(vm.serverTitle) - .toolbar { mainToolbar } + .toolbar { if !embedded { mainToolbar } } .sheet(isPresented: $vm.connectionSheetOpen) { ConnectionSheet(vm: vm) { vm.connectionSheetOpen = false } } - .frame(minWidth: 380, minHeight: 520) + // Fill the host: keep a sensible minimum for the standalone window, but expand to fill + // the Amateur Radio Suite's host pane (out-of-process) instead of floating undersized. + .frame(minWidth: 380, maxWidth: .infinity, minHeight: 520, maxHeight: .infinity) .background(Color(NSColor.windowBackgroundColor)) } + /// Inline equivalent of `mainToolbar`, shown when hosted in the Suite (no window toolbar). + private var embeddedToolbar: some View { + VStack(spacing: 0) { + HStack(spacing: 12) { + ConnectionBadge(state: vm.connection, host: hostHint) + .equatable() + .help(hostHint) + Spacer(minLength: 12) + BackendBadge(backend: vm.backend) + .equatable() + Spacer(minLength: 12) + Button { vm.connectionSheetOpen = true } label: { + Image(systemName: "network.badge.shield.half.filled") + } + .help("Server connection settings") + Button { + vm.toggleSetup() + if vm.setupOpen { Task { await vm.refreshLogLevel() } } + } label: { + Image(systemName: vm.setupOpen ? "wrench.and.screwdriver.fill" : "wrench.and.screwdriver") + } + .help("Open SETUP overlay") + } + .buttonStyle(.borderless) + .padding(.horizontal, 12) + .padding(.vertical, 8) + Divider() + } + } + // MARK: - Toolbar @ToolbarContentBuilder @@ -121,7 +160,7 @@ struct ContentView: View { } } .padding(10) - .frame(maxWidth: .infinity, alignment: .top) + .frame(maxWidth: .infinity, maxHeight: .infinity, alignment: .top) .background(Color(NSColor.windowBackgroundColor)) }