Kaynağa Gözat

Refine titlebar browser chrome layout

Move browser controls into a native-style titlebar layout with a centered site capsule, and update web content insets so pages render below the titlebar controls.

Made-with: Cursor
huzaifahayat12 3 ay önce
ebeveyn
işleme
7cb58a607b
1 değiştirilmiş dosya ile 171 ekleme ve 221 silme
  1. 171 221
      google_apps/InAppBrowserWindow.swift

+ 171 - 221
google_apps/InAppBrowserWindow.swift

@@ -27,7 +27,7 @@ final class InAppBrowserWindowController: NSWindowController, NSWindowDelegate {
     private let initialURL: URL
     private var didLoadInitialURL = false
 
-    // Toolbar UI (top navigation bar)
+    // Titlebar controls
     private var backButton: NSButton!
     private var forwardButton: NSButton!
     private var reloadButton: NSButton!
@@ -38,19 +38,19 @@ final class InAppBrowserWindowController: NSWindowController, NSWindowDelegate {
     private var handButton: NSButton!
     private var downloadButton: NSButton!
     private var moreButton: NSButton!
-    private var urlField: NSTextField!
-    private var isUpdatingURLField = false
-    private var didSubmitURLFromAction = false
+    private var siteNameLabel: NSTextField!
+    private var siteSubtitleLabel: NSTextField!
+    private var siteIconView: NSImageView!
     private var isPinnedOnTop = false
     private var isGestureModeEnabled = true
 
     private func setToggledStyle(_ button: NSButton, isOn: Bool) {
-        button.layer?.backgroundColor = isOn
-            ? NSColor.systemBlue.withAlphaComponent(0.28).cgColor
-            : NSColor.white.withAlphaComponent(0.06).cgColor
-        button.layer?.borderColor = isOn
-            ? NSColor.systemBlue.withAlphaComponent(0.55).cgColor
-            : NSColor.white.withAlphaComponent(0.10).cgColor
+        // Native-ish “selected” look using tint + filled variant when available.
+        if isOn {
+            button.contentTintColor = .systemBlue
+        } else {
+            button.contentTintColor = nil
+        }
     }
 
     init(initialURL: URL, windowTitle: String?) {
@@ -68,76 +68,39 @@ final class InAppBrowserWindowController: NSWindowController, NSWindowDelegate {
         )
         window.title = windowTitle ?? "Browser"
         window.isReleasedWhenClosed = false
+        window.titleVisibility = .hidden
+        window.titlebarAppearsTransparent = true
+        window.styleMask.insert(.fullSizeContentView)
 
-        // Build UI: top toolbar + web view underneath.
+        // Build UI: web view fills content; controls live in titlebar.
         let containerView = NSView(frame: .zero)
         containerView.wantsLayer = true
         containerView.layer?.backgroundColor = NSColor.clear.cgColor
         window.contentView = containerView
 
-        let toolbarHeight: CGFloat = 46
-        let toolbarContainer = NSView()
-        toolbarContainer.translatesAutoresizingMaskIntoConstraints = false
-        toolbarContainer.wantsLayer = true
-        toolbarContainer.layer?.cornerRadius = 14
-        toolbarContainer.layer?.masksToBounds = true
-        containerView.addSubview(toolbarContainer)
-
-        // Blurred background like a modern browser toolbar.
-        let toolbarBlur = NSVisualEffectView()
-        toolbarBlur.material = .hudWindow
-        toolbarBlur.blendingMode = .behindWindow
-        toolbarBlur.state = .active
-        toolbarBlur.translatesAutoresizingMaskIntoConstraints = false
-        toolbarContainer.addSubview(toolbarBlur)
-
-        let toolbarView = NSStackView()
-        toolbarView.orientation = .horizontal
-        toolbarView.alignment = .centerY
-        toolbarView.spacing = 10
-        toolbarView.translatesAutoresizingMaskIntoConstraints = false
-        toolbarContainer.addSubview(toolbarView)
-
-        toolbarBlur.wantsLayer = true
-        toolbarBlur.layer?.cornerRadius = 14
-        toolbarBlur.layer?.masksToBounds = true
-
         func makeIconButton(symbolName: String, accessibility: String, action: Selector, fallbackSymbolName: String? = nil) -> NSButton {
             let button = NSButton()
             let primary = NSImage(systemSymbolName: symbolName, accessibilityDescription: accessibility)
             let fallback = fallbackSymbolName.flatMap { NSImage(systemSymbolName: $0, accessibilityDescription: accessibility) }
             button.image = primary ?? fallback
             button.image?.isTemplate = true
-            button.isBordered = false
-            button.contentTintColor = .white
             button.imagePosition = .imageOnly
             button.imageScaling = .scaleProportionallyDown
-            button.setButtonType(.momentaryChange)
+            button.isBordered = true
+            button.bezelStyle = .texturedRounded
+            button.controlSize = .small
+            button.font = NSFont.systemFont(ofSize: 12, weight: .semibold)
+            button.setButtonType(.momentaryPushIn)
             button.action = action
-            button.setButtonType(.momentaryChange)
-
-            // Subtle circular hit target like the screenshot.
-            button.wantsLayer = true
-            button.layer?.cornerRadius = 12
-            button.layer?.masksToBounds = true
-            button.layer?.backgroundColor = NSColor.white.withAlphaComponent(0.06).cgColor
-            button.layer?.borderColor = NSColor.white.withAlphaComponent(0.10).cgColor
-            button.layer?.borderWidth = 1
-
-            button.translatesAutoresizingMaskIntoConstraints = false
-            button.widthAnchor.constraint(equalToConstant: 26).isActive = true
-            button.heightAnchor.constraint(equalToConstant: 26).isActive = true
-
+            button.toolTip = accessibility
             if button.image == nil {
-                // Last-resort fallback if SF Symbols aren’t available for some reason.
                 button.title = "⋮"
-                button.font = NSFont.systemFont(ofSize: 14, weight: .semibold)
-                button.contentTintColor = NSColor.white.withAlphaComponent(0.9)
             } else {
                 button.title = ""
             }
-
-            button.toolTip = accessibility
+            // Consistent symbol sizing.
+            let cfg = NSImage.SymbolConfiguration(pointSize: 12, weight: .semibold)
+            button.image = button.image?.withSymbolConfiguration(cfg)
             return button
         }
 
@@ -183,54 +146,37 @@ final class InAppBrowserWindowController: NSWindowController, NSWindowDelegate {
             action: #selector(showLauncher)
         )
 
-        // URL "pill" wrapper.
-        let urlWrapperView = NSView()
-        urlWrapperView.translatesAutoresizingMaskIntoConstraints = false
-        urlWrapperView.wantsLayer = true
-        urlWrapperView.layer?.cornerRadius = 12
-        urlWrapperView.layer?.masksToBounds = true
-        urlWrapperView.layer?.backgroundColor = NSColor.white.withAlphaComponent(0.07).cgColor
-        urlWrapperView.layer?.borderColor = NSColor.white.withAlphaComponent(0.18).cgColor
-        urlWrapperView.layer?.borderWidth = 1
-
-        // URL field (inside pill)
-        urlField = NSTextField()
-        urlField.translatesAutoresizingMaskIntoConstraints = false
-        urlField.action = #selector(urlFieldDidSubmit)
-        urlField.placeholderString = "Search or enter website name"
-        urlField.font = NSFont.systemFont(ofSize: 13, weight: .medium)
-        urlField.textColor = .white
-        urlField.alignment = .left
-        urlField.isEditable = true
-        urlField.isBordered = false
-        urlField.drawsBackground = false
-        urlField.focusRingType = .none
-
-        // Optional "lock" glyph like browsers (purely visual).
-        let lockImageView = NSImageView()
-        lockImageView.translatesAutoresizingMaskIntoConstraints = false
-        lockImageView.image = NSImage(systemSymbolName: "lock.fill", accessibilityDescription: "Secure")
-        lockImageView.symbolConfiguration = NSImage.SymbolConfiguration(pointSize: 11, weight: .semibold)
-        lockImageView.contentTintColor = NSColor.white.withAlphaComponent(0.7)
-        lockImageView.imageScaling = .scaleProportionallyDown
-
-        let urlInnerStack = NSStackView(views: [lockImageView, urlField])
-        urlInnerStack.orientation = .horizontal
-        urlInnerStack.alignment = .centerY
-        urlInnerStack.spacing = 8
-        urlInnerStack.translatesAutoresizingMaskIntoConstraints = false
-
-        urlWrapperView.addSubview(urlInnerStack)
-        NSLayoutConstraint.activate([
-            lockImageView.widthAnchor.constraint(equalToConstant: 14),
-            lockImageView.heightAnchor.constraint(equalToConstant: 14),
-
-            urlInnerStack.leadingAnchor.constraint(equalTo: urlWrapperView.leadingAnchor, constant: 12),
-            urlInnerStack.trailingAnchor.constraint(equalTo: urlWrapperView.trailingAnchor, constant: -12),
-            urlInnerStack.topAnchor.constraint(equalTo: urlWrapperView.topAnchor, constant: 6),
-            urlInnerStack.bottomAnchor.constraint(equalTo: urlWrapperView.bottomAnchor, constant: -6),
-            urlWrapperView.heightAnchor.constraint(equalToConstant: 30),
-        ])
+        // Center capsule (icon + site + optional page title).
+        siteIconView = NSImageView()
+        siteIconView.translatesAutoresizingMaskIntoConstraints = false
+        siteIconView.image = NSImage(systemSymbolName: "globe", accessibilityDescription: "Site")?
+            .withSymbolConfiguration(NSImage.SymbolConfiguration(pointSize: 12, weight: .semibold))
+        siteIconView.contentTintColor = NSColor.secondaryLabelColor
+        siteIconView.imageScaling = .scaleProportionallyDown
+
+        siteNameLabel = NSTextField(labelWithString: windowTitle ?? "Browser")
+        siteNameLabel.font = NSFont.systemFont(ofSize: 13, weight: .semibold)
+        siteNameLabel.textColor = NSColor.labelColor
+        siteNameLabel.alignment = .left
+        siteNameLabel.lineBreakMode = .byTruncatingTail
+
+        siteSubtitleLabel = NSTextField(labelWithString: "")
+        siteSubtitleLabel.font = NSFont.systemFont(ofSize: 10.5, weight: .medium)
+        siteSubtitleLabel.textColor = NSColor.secondaryLabelColor
+        siteSubtitleLabel.alignment = .left
+        siteSubtitleLabel.lineBreakMode = .byTruncatingTail
+
+        let textStack = NSStackView(views: [siteNameLabel, siteSubtitleLabel])
+        textStack.orientation = .vertical
+        textStack.alignment = .leading
+        textStack.spacing = 0
+        textStack.translatesAutoresizingMaskIntoConstraints = false
+
+        let headerStack = NSStackView(views: [siteIconView, textStack])
+        headerStack.orientation = .horizontal
+        headerStack.alignment = .centerY
+        headerStack.spacing = 8
+        headerStack.translatesAutoresizingMaskIntoConstraints = false
 
         // Open in external browser
         openInBrowserButton = makeIconButton(
@@ -261,56 +207,28 @@ final class InAppBrowserWindowController: NSWindowController, NSWindowDelegate {
             fallbackSymbolName: "ellipsis"
         )
 
-        toolbarView.addArrangedSubview(backButton)
-        toolbarView.addArrangedSubview(forwardButton)
-        toolbarView.addArrangedSubview(reloadButton)
-        toolbarView.addArrangedSubview(urlWrapperView)
-        toolbarView.addArrangedSubview(pinButton)
-        toolbarView.addArrangedSubview(homeButton)
-        toolbarView.addArrangedSubview(gridButton)
-        toolbarView.addArrangedSubview(openInBrowserButton)
-        toolbarView.addArrangedSubview(handButton)
-        toolbarView.addArrangedSubview(downloadButton)
-        toolbarView.addArrangedSubview(moreButton)
-
-        // Give the URL field more space than the icon buttons.
-        urlWrapperView.setContentHuggingPriority(.defaultLow, for: .horizontal)
-        urlWrapperView.setContentCompressionResistancePriority(.defaultHigh, for: .horizontal)
-
-        // (URL layout constraints are defined via urlInnerStack above)
-
-        // Web view below toolbar.
+        // Web view fills content.
         webView.translatesAutoresizingMaskIntoConstraints = false
         webView.setValue(false, forKey: "drawsBackground")
 
         containerView.addSubview(webView)
 
+        // Use the window content layout guide so content starts below titlebar.
+        let contentGuide = (window.contentLayoutGuide as? NSLayoutGuide) ?? NSLayoutGuide()
         NSLayoutConstraint.activate([
-            toolbarContainer.leadingAnchor.constraint(equalTo: containerView.leadingAnchor, constant: 10),
-            toolbarContainer.trailingAnchor.constraint(equalTo: containerView.trailingAnchor, constant: -10),
-            toolbarContainer.topAnchor.constraint(equalTo: containerView.topAnchor, constant: 10),
-            toolbarContainer.heightAnchor.constraint(equalToConstant: toolbarHeight),
-
-            toolbarBlur.leadingAnchor.constraint(equalTo: toolbarContainer.leadingAnchor),
-            toolbarBlur.trailingAnchor.constraint(equalTo: toolbarContainer.trailingAnchor),
-            toolbarBlur.topAnchor.constraint(equalTo: toolbarContainer.topAnchor),
-            toolbarBlur.bottomAnchor.constraint(equalTo: toolbarContainer.bottomAnchor),
-
-            toolbarView.leadingAnchor.constraint(equalTo: toolbarContainer.leadingAnchor, constant: 12),
-            toolbarView.trailingAnchor.constraint(equalTo: toolbarContainer.trailingAnchor, constant: -12),
-            toolbarView.topAnchor.constraint(equalTo: toolbarContainer.topAnchor, constant: 6),
-            toolbarView.bottomAnchor.constraint(equalTo: toolbarContainer.bottomAnchor, constant: -6),
-
             webView.leadingAnchor.constraint(equalTo: containerView.leadingAnchor),
             webView.trailingAnchor.constraint(equalTo: containerView.trailingAnchor),
             webView.bottomAnchor.constraint(equalTo: containerView.bottomAnchor),
-            webView.topAnchor.constraint(equalTo: toolbarContainer.bottomAnchor, constant: 0),
+            webView.topAnchor.constraint(equalTo: contentGuide.topAnchor),
         ])
 
         super.init(window: window)
 
         window.delegate = self
 
+        // Build titlebar layout after super.init.
+        configureTitlebarLayout(centerView: headerStack)
+
         // Safe to reference `self` only after super.init.
         backButton.target = self
         forwardButton.target = self
@@ -322,8 +240,6 @@ final class InAppBrowserWindowController: NSWindowController, NSWindowDelegate {
         handButton.target = self
         downloadButton.target = self
         moreButton.target = self
-        urlField.delegate = self
-        urlField.target = self
 
         webView.uiDelegate = self
         webView.navigationDelegate = self
@@ -333,10 +249,109 @@ final class InAppBrowserWindowController: NSWindowController, NSWindowDelegate {
         // Initialize toolbar state.
         setToggledStyle(pinButton, isOn: isPinnedOnTop)
         setToggledStyle(handButton, isOn: isGestureModeEnabled)
-        urlField.stringValue = initialURL.absoluteString
+        siteNameLabel.stringValue = windowTitle ?? (initialURL.host ?? "Browser")
         updateToolbarState()
     }
 
+    private func configureTitlebarLayout(centerView: NSView) {
+        guard let window else { return }
+
+        let centerCapsule = NSVisualEffectView()
+        centerCapsule.material = .titlebar
+        centerCapsule.blendingMode = .withinWindow
+        centerCapsule.state = .active
+        centerCapsule.wantsLayer = true
+        centerCapsule.layer?.cornerRadius = 12
+        centerCapsule.layer?.masksToBounds = true
+        centerCapsule.layer?.borderWidth = 1
+        centerCapsule.layer?.borderColor = NSColor.separatorColor.withAlphaComponent(0.6).cgColor
+        centerCapsule.translatesAutoresizingMaskIntoConstraints = false
+
+        centerCapsule.addSubview(centerView)
+        centerView.translatesAutoresizingMaskIntoConstraints = false
+        NSLayoutConstraint.activate([
+            centerView.leadingAnchor.constraint(equalTo: centerCapsule.leadingAnchor, constant: 12),
+            centerView.trailingAnchor.constraint(equalTo: centerCapsule.trailingAnchor, constant: -12),
+            centerView.topAnchor.constraint(equalTo: centerCapsule.topAnchor, constant: 5),
+            centerView.bottomAnchor.constraint(equalTo: centerCapsule.bottomAnchor, constant: -5),
+            centerCapsule.heightAnchor.constraint(equalToConstant: 30),
+            centerCapsule.widthAnchor.constraint(greaterThanOrEqualToConstant: 240),
+            centerCapsule.widthAnchor.constraint(lessThanOrEqualToConstant: 520),
+        ])
+
+        let leftStack = NSStackView(views: [backButton, forwardButton, reloadButton, homeButton])
+        leftStack.orientation = .horizontal
+        leftStack.alignment = .centerY
+        leftStack.spacing = 6
+
+        let rightStack = NSStackView(views: [
+            pinButton,
+            gridButton,
+            openInBrowserButton,
+            handButton,
+            downloadButton,
+            moreButton,
+        ])
+        rightStack.orientation = .horizontal
+        rightStack.alignment = .centerY
+        rightStack.spacing = 6
+
+        func accessory(_ view: NSView, layout: NSLayoutConstraint.Attribute) -> NSTitlebarAccessoryViewController {
+            let vc = NSTitlebarAccessoryViewController()
+            vc.layoutAttribute = layout
+            vc.view = view
+            return vc
+        }
+
+        // Single top accessory with left / center / right.
+        let topBar = NSView(frame: NSRect(x: 0, y: 0, width: 1000, height: 34))
+        topBar.translatesAutoresizingMaskIntoConstraints = false
+
+        let leftContainer = NSView()
+        leftContainer.translatesAutoresizingMaskIntoConstraints = false
+        leftContainer.addSubview(leftStack)
+        leftStack.translatesAutoresizingMaskIntoConstraints = false
+
+        let centerContainer = NSView()
+        centerContainer.translatesAutoresizingMaskIntoConstraints = false
+        centerContainer.addSubview(centerCapsule)
+
+        let rightContainer = NSView()
+        rightContainer.translatesAutoresizingMaskIntoConstraints = false
+        rightContainer.addSubview(rightStack)
+        rightStack.translatesAutoresizingMaskIntoConstraints = false
+
+        let barStack = NSStackView(views: [leftContainer, centerContainer, rightContainer])
+        barStack.orientation = .horizontal
+        barStack.alignment = .centerY
+        barStack.distribution = .fill
+        barStack.spacing = 10
+        barStack.translatesAutoresizingMaskIntoConstraints = false
+
+        topBar.addSubview(barStack)
+        NSLayoutConstraint.activate([
+            topBar.heightAnchor.constraint(equalToConstant: 34),
+
+            barStack.leadingAnchor.constraint(equalTo: topBar.leadingAnchor, constant: 12),
+            barStack.trailingAnchor.constraint(equalTo: topBar.trailingAnchor, constant: -12),
+            barStack.topAnchor.constraint(equalTo: topBar.topAnchor, constant: 2),
+            barStack.bottomAnchor.constraint(equalTo: topBar.bottomAnchor, constant: -2),
+
+            leftContainer.widthAnchor.constraint(equalTo: rightContainer.widthAnchor),
+
+            leftStack.leadingAnchor.constraint(equalTo: leftContainer.leadingAnchor),
+            leftStack.centerYAnchor.constraint(equalTo: leftContainer.centerYAnchor),
+
+            rightStack.trailingAnchor.constraint(equalTo: rightContainer.trailingAnchor),
+            rightStack.centerYAnchor.constraint(equalTo: rightContainer.centerYAnchor),
+
+            centerCapsule.centerXAnchor.constraint(equalTo: centerContainer.centerXAnchor),
+            centerCapsule.centerYAnchor.constraint(equalTo: centerContainer.centerYAnchor),
+        ])
+
+        window.addTitlebarAccessoryViewController(accessory(topBar, layout: .top))
+    }
+
     @available(*, unavailable)
     required init?(coder: NSCoder) {
         nil
@@ -459,66 +474,15 @@ final class InAppBrowserWindowController: NSWindowController, NSWindowDelegate {
         window?.performClose(nil)
     }
 
-    @objc private func urlFieldDidSubmit() {
-        didSubmitURLFromAction = true
-        loadFromURLField()
-    }
-
-    private func loadFromURLField() {
-        let raw = urlField.stringValue.trimmingCharacters(in: .whitespacesAndNewlines)
-        guard !raw.isEmpty else { return }
-
-        let normalized: String
-        if raw.lowercased().hasPrefix("http://") || raw.lowercased().hasPrefix("https://") {
-            normalized = raw
-        } else {
-            normalized = "https://\(raw)"
-        }
-
-        guard let url = URL(string: normalized) else {
-            revertURLFieldToCurrentWebURL()
-            return
-        }
-
-        // Keep normalized URL visible immediately after submit.
-        isUpdatingURLField = true
-        urlField.stringValue = normalized
-        isUpdatingURLField = false
+    private func updateSiteNameFromWebView() {
+        let host = webView.url?.host?.trimmingCharacters(in: .whitespacesAndNewlines)
+        let pageTitle = webView.title?.trimmingCharacters(in: .whitespacesAndNewlines)
+        let preferred = (host?.isEmpty == false) ? host! : ((pageTitle?.isEmpty == false) ? pageTitle! : "Browser")
 
-        webView.load(URLRequest(url: url))
-    }
-
-    private func revertURLFieldToCurrentWebURL() {
-        guard let url = webView.url?.absoluteString else { return }
-        isUpdatingURLField = true
-        urlField.stringValue = url
-        isUpdatingURLField = false
-    }
-
-    private var isURLFieldBeingEdited: Bool {
-        urlField.currentEditor() != nil
-    }
-
-    private func setURLFieldToCurrentWebURL() {
-        guard let url = webView.url?.absoluteString else { return }
-        guard !isUpdatingURLField else { return }
-
-        // Avoid overriding manual typing unless this update follows a submit.
-        // However, right after navigation starts the text field can still be
-        // considered "editing" (first responder), which would otherwise keep
-        // the URL pill blank/stale. If it's empty, we should populate it.
-        if isURLFieldBeingEdited && !didSubmitURLFromAction {
-            if urlField.stringValue.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty {
-                // proceed
-            } else {
-                return
-            }
-        }
-
-        isUpdatingURLField = true
-        urlField.stringValue = url
-        isUpdatingURLField = false
-        didSubmitURLFromAction = false
+        siteNameLabel.stringValue = preferred
+        siteSubtitleLabel.stringValue = (pageTitle?.isEmpty == false && pageTitle != preferred) ? pageTitle! : ""
+        siteSubtitleLabel.isHidden = siteSubtitleLabel.stringValue.isEmpty
+        window?.title = preferred
     }
 
     private func updateToolbarState() {
@@ -528,8 +492,8 @@ final class InAppBrowserWindowController: NSWindowController, NSWindowDelegate {
         // Keep reload enabled; it matches typical browser behavior.
         reloadButton.isEnabled = true
 
-        // Sync URL field and window title.
-        setURLFieldToCurrentWebURL()
+        // Sync center site name.
+        updateSiteNameFromWebView()
     }
 
     private func showAlert(title: String, message: String) {
@@ -602,20 +566,6 @@ extension InAppBrowserWindowController: WKNavigationDelegate {
     }
 }
 
-extension InAppBrowserWindowController: NSTextFieldDelegate {
-    func controlTextDidEndEditing(_ obj: Notification) {
-        guard obj.object as? NSTextField === urlField else { return }
-        guard !isUpdatingURLField else { return }
-        // Return key triggers both action and end-editing callbacks.
-        // Prevent duplicate loads so history/back-forward stays stable.
-        if didSubmitURLFromAction {
-            didSubmitURLFromAction = false
-            return
-        }
-        loadFromURLField()
-    }
-}
-
 extension InAppBrowserWindowController: WKUIDelegate {
     func webView(
         _ webView: WKWebView,