From ed0d24603968a9c2c122c49c82378a6f8ceeeccb Mon Sep 17 00:00:00 2001 From: Lawrence Chen <54008264+lawrencecchen@users.noreply.github.com> Date: Sat, 21 Feb 2026 04:39:27 -0800 Subject: [PATCH] Slim browser omnibar and add button hover/press states (#271) --- Sources/Panels/BrowserPanelView.swift | 46 +++++++- ...owser_omnibar_compact_layout_regression.py | 109 ++++++++++++++++++ 2 files changed, 149 insertions(+), 6 deletions(-) create mode 100644 tests/test_browser_omnibar_compact_layout_regression.py diff --git a/Sources/Panels/BrowserPanelView.swift b/Sources/Panels/BrowserPanelView.swift index 418714cf..a4212818 100644 --- a/Sources/Panels/BrowserPanelView.swift +++ b/Sources/Panels/BrowserPanelView.swift @@ -122,6 +122,39 @@ struct OmnibarInlineCompletion: Equatable { } } +private struct OmnibarAddressButtonStyle: ButtonStyle { + func makeBody(configuration: Configuration) -> some View { + OmnibarAddressButtonStyleBody(configuration: configuration) + } +} + +private struct OmnibarAddressButtonStyleBody: View { + let configuration: OmnibarAddressButtonStyle.Configuration + + @Environment(\.isEnabled) private var isEnabled + @State private var isHovered = false + + private var backgroundOpacity: Double { + guard isEnabled else { return 0.0 } + if configuration.isPressed { return 0.16 } + if isHovered { return 0.08 } + return 0.0 + } + + var body: some View { + configuration.label + .background( + RoundedRectangle(cornerRadius: 8, style: .continuous) + .fill(Color.primary.opacity(backgroundOpacity)) + ) + .onHover { hovering in + isHovered = hovering + } + .animation(.easeOut(duration: 0.12), value: isHovered) + .animation(.easeOut(duration: 0.08), value: configuration.isPressed) + } +} + /// View for rendering a browser panel with address bar struct BrowserPanelView: View { @ObservedObject var panel: BrowserPanel @@ -149,7 +182,8 @@ struct BrowserPanelView: View { @State private var lastHandledAddressBarFocusRequestId: UUID? private let omnibarPillCornerRadius: CGFloat = 12 private let addressBarButtonSize: CGFloat = 22 - private let addressBarButtonHitSize: CGFloat = 32 + private let addressBarButtonHitSize: CGFloat = 26 + private let addressBarVerticalPadding: CGFloat = 4 private let devToolsButtonIconSize: CGFloat = 11 private var searchEngine: BrowserSearchEngine { @@ -335,7 +369,7 @@ struct BrowserPanelView: View { developerToolsButton } .padding(.horizontal, 8) - .padding(.vertical, 6) + .padding(.vertical, addressBarVerticalPadding) .background(Color(nsColor: GhosttyApp.shared.defaultBackgroundColor)) // Keep the omnibar stack above WKWebView so the suggestions popup is visible. .zIndex(1) @@ -354,7 +388,7 @@ struct BrowserPanelView: View { .frame(width: addressBarButtonHitSize, height: addressBarButtonHitSize, alignment: .center) .contentShape(Rectangle()) } - .buttonStyle(.plain) + .buttonStyle(OmnibarAddressButtonStyle()) .disabled(!panel.canGoBack) .opacity(panel.canGoBack ? 1.0 : 0.4) .help("Go Back") @@ -370,7 +404,7 @@ struct BrowserPanelView: View { .frame(width: addressBarButtonHitSize, height: addressBarButtonHitSize, alignment: .center) .contentShape(Rectangle()) } - .buttonStyle(.plain) + .buttonStyle(OmnibarAddressButtonStyle()) .disabled(!panel.canGoForward) .opacity(panel.canGoForward ? 1.0 : 0.4) .help("Go Forward") @@ -393,7 +427,7 @@ struct BrowserPanelView: View { .frame(width: addressBarButtonHitSize, height: addressBarButtonHitSize, alignment: .center) .contentShape(Rectangle()) } - .buttonStyle(.plain) + .buttonStyle(OmnibarAddressButtonStyle()) .help(panel.isLoading ? "Stop" : "Reload") if panel.isDownloading { @@ -419,7 +453,7 @@ struct BrowserPanelView: View { .foregroundStyle(devToolsColorOption.color) .frame(width: addressBarButtonSize, height: addressBarButtonSize, alignment: .center) } - .buttonStyle(.plain) + .buttonStyle(OmnibarAddressButtonStyle()) .frame(width: addressBarButtonSize, height: addressBarButtonSize, alignment: .center) .help("Toggle Developer Tools") .accessibilityIdentifier("BrowserToggleDevToolsButton") diff --git a/tests/test_browser_omnibar_compact_layout_regression.py b/tests/test_browser_omnibar_compact_layout_regression.py new file mode 100644 index 00000000..3886f495 --- /dev/null +++ b/tests/test_browser_omnibar_compact_layout_regression.py @@ -0,0 +1,109 @@ +#!/usr/bin/env python3 +"""Static regression guards for compact browser omnibar sizing.""" + +from __future__ import annotations + +import re +import subprocess +from pathlib import Path + + +def repo_root() -> Path: + result = subprocess.run( + ["git", "rev-parse", "--show-toplevel"], + capture_output=True, + text=True, + ) + if result.returncode == 0: + return Path(result.stdout.strip()) + return Path(__file__).resolve().parents[1] + + +def extract_block(source: str, signature: str) -> str: + start = source.find(signature) + if start < 0: + raise ValueError(f"Missing signature: {signature}") + brace_start = source.find("{", start) + if brace_start < 0: + raise ValueError(f"Missing opening brace for: {signature}") + depth = 0 + for idx in range(brace_start, len(source)): + char = source[idx] + if char == "{": + depth += 1 + elif char == "}": + depth -= 1 + if depth == 0: + return source[brace_start : idx + 1] + raise ValueError(f"Unbalanced braces for: {signature}") + + +def parse_cgfloat_constant(source: str, name: str) -> float | None: + match = re.search( + rf"private let {re.escape(name)}: CGFloat = ([0-9]+(?:\.[0-9]+)?)", + source, + ) + if not match: + return None + return float(match.group(1)) + + +def main() -> int: + root = repo_root() + failures: list[str] = [] + + view_path = root / "Sources" / "Panels" / "BrowserPanelView.swift" + view_source = view_path.read_text(encoding="utf-8") + + hit_size = parse_cgfloat_constant(view_source, "addressBarButtonHitSize") + if hit_size is None: + failures.append("addressBarButtonHitSize constant is missing") + elif hit_size > 26: + failures.append( + f"addressBarButtonHitSize regressed to {hit_size:g}; expected <= 26 for compact omnibar height" + ) + + vertical_padding = parse_cgfloat_constant(view_source, "addressBarVerticalPadding") + if vertical_padding is None: + failures.append("addressBarVerticalPadding constant is missing") + elif vertical_padding > 4: + failures.append( + f"addressBarVerticalPadding regressed to {vertical_padding:g}; expected <= 4 for compact omnibar height" + ) + + address_bar_block = extract_block(view_source, "private var addressBar: some View") + if ".padding(.vertical, addressBarVerticalPadding)" not in address_bar_block: + failures.append("addressBar no longer applies compact vertical padding via addressBarVerticalPadding") + + button_bar_block = extract_block(view_source, "private var addressBarButtonBar: some View") + hit_frame_uses = button_bar_block.count("addressBarButtonHitSize") + if hit_frame_uses < 3: + failures.append( + "navigation buttons no longer consistently use addressBarButtonHitSize frames (padding may be lost)" + ) + + extract_block(view_source, "private struct OmnibarAddressButtonStyle: ButtonStyle") + style_body_block = extract_block(view_source, "private struct OmnibarAddressButtonStyleBody: View") + if "configuration.isPressed" not in style_body_block: + failures.append("OmnibarAddressButtonStyleBody is missing pressed-state styling") + if "isHovered" not in style_body_block or ".onHover" not in style_body_block: + failures.append("OmnibarAddressButtonStyleBody is missing hover-state styling") + + style_uses = view_source.count(".buttonStyle(OmnibarAddressButtonStyle())") + if style_uses < 4: + failures.append( + "address bar buttons no longer consistently use OmnibarAddressButtonStyle" + ) + + if failures: + print("FAIL: browser omnibar compact layout regression guards failed") + for failure in failures: + print(f" - {failure}") + return 1 + + print("PASS: browser omnibar compact layout regression guards are in place") + return 0 + + +if __name__ == "__main__": + raise SystemExit(main())