Use workspace color for notification ring and selection bar (#664)
- Notification/focus flash uses workspace customColor (fallback: accent) - Selection bar/indicator uses workspace customColor when set - Flash color propagated through Panel.triggerFlash(color:) API - Browser panel flash overlay uses workspace color - Regression tests for flash color resolution Fixes https://github.com/manaflow-ai/cmux/issues/557
This commit is contained in:
parent
181574586e
commit
4bfe95d125
8 changed files with 86 additions and 15 deletions
|
|
@ -6437,6 +6437,10 @@ private struct TabItemView: View {
|
|||
usesInvertedActiveForeground ? Color.white.opacity(0.8) : cmuxAccentColor()
|
||||
}
|
||||
|
||||
private var activeSelectionColor: Color {
|
||||
resolvedCustomTabColor ?? Color(nsColor: sidebarSelectedWorkspaceBackgroundNSColor(for: colorScheme))
|
||||
}
|
||||
|
||||
private var shortcutHintEmphasis: Double {
|
||||
usesInvertedActiveForeground ? 1.0 : 0.9
|
||||
}
|
||||
|
|
@ -6491,6 +6495,7 @@ private struct TabItemView: View {
|
|||
.foregroundColor(activeSecondaryColor(0.8))
|
||||
}
|
||||
|
||||
|
||||
Text(tab.title)
|
||||
.font(.system(size: 12.5, weight: titleFontWeight))
|
||||
.foregroundColor(activePrimaryTextColor)
|
||||
|
|
@ -6948,11 +6953,11 @@ private struct TabItemView: View {
|
|||
private var backgroundColor: Color {
|
||||
switch activeTabIndicatorStyle {
|
||||
case .leftRail:
|
||||
if isActive { return Color(nsColor: sidebarSelectedWorkspaceBackgroundNSColor(for: colorScheme)) }
|
||||
if isActive { return activeSelectionColor }
|
||||
if isMultiSelected { return cmuxAccentColor().opacity(0.25) }
|
||||
return Color.clear
|
||||
case .solidFill:
|
||||
if isActive { return Color(nsColor: sidebarSelectedWorkspaceBackgroundNSColor(for: colorScheme)) }
|
||||
if isActive { return activeSelectionColor }
|
||||
if let custom = resolvedCustomTabColor {
|
||||
if isMultiSelected { return custom.opacity(0.35) }
|
||||
return custom.opacity(0.7)
|
||||
|
|
|
|||
|
|
@ -3951,11 +3951,11 @@ final class GhosttySurfaceScrollView: NSView {
|
|||
notificationRingOverlayView.layer?.masksToBounds = false
|
||||
notificationRingOverlayView.autoresizingMask = [.width, .height]
|
||||
notificationRingLayer.fillColor = NSColor.clear.cgColor
|
||||
notificationRingLayer.strokeColor = NSColor.systemBlue.cgColor
|
||||
notificationRingLayer.strokeColor = cmuxAccentNSColor().cgColor
|
||||
notificationRingLayer.lineWidth = 2.5
|
||||
notificationRingLayer.lineJoin = .round
|
||||
notificationRingLayer.lineCap = .round
|
||||
notificationRingLayer.shadowColor = NSColor.systemBlue.cgColor
|
||||
notificationRingLayer.shadowColor = cmuxAccentNSColor().cgColor
|
||||
notificationRingLayer.shadowOpacity = 0.35
|
||||
notificationRingLayer.shadowRadius = 3
|
||||
notificationRingLayer.shadowOffset = .zero
|
||||
|
|
@ -3968,11 +3968,11 @@ final class GhosttySurfaceScrollView: NSView {
|
|||
flashOverlayView.layer?.masksToBounds = false
|
||||
flashOverlayView.autoresizingMask = [.width, .height]
|
||||
flashLayer.fillColor = NSColor.clear.cgColor
|
||||
flashLayer.strokeColor = NSColor.systemBlue.cgColor
|
||||
flashLayer.strokeColor = cmuxAccentNSColor().cgColor
|
||||
flashLayer.lineWidth = 3
|
||||
flashLayer.lineJoin = .round
|
||||
flashLayer.lineCap = .round
|
||||
flashLayer.shadowColor = NSColor.systemBlue.cgColor
|
||||
flashLayer.shadowColor = cmuxAccentNSColor().cgColor
|
||||
flashLayer.shadowOpacity = 0.6
|
||||
flashLayer.shadowRadius = 6
|
||||
flashLayer.shadowOffset = .zero
|
||||
|
|
@ -4145,6 +4145,27 @@ final class GhosttySurfaceScrollView: NSView {
|
|||
surfaceView.onTriggerFlash = handler
|
||||
}
|
||||
|
||||
private func resolvedWorkspaceFocusFlashColor() -> NSColor {
|
||||
guard let tabId = surfaceView.tabId,
|
||||
let app = AppDelegate.shared,
|
||||
let manager = app.tabManagerFor(tabId: tabId) ?? app.tabManager,
|
||||
let workspace = manager.tabs.first(where: { $0.id == tabId }) else {
|
||||
return cmuxAccentNSColor()
|
||||
}
|
||||
return Workspace.resolvedFocusFlashColor(customColorHex: workspace.customColor)
|
||||
}
|
||||
|
||||
private func setFocusFlashColor(_ color: NSColor?) {
|
||||
let resolved = color ?? resolvedWorkspaceFocusFlashColor()
|
||||
CATransaction.begin()
|
||||
CATransaction.setDisableActions(true)
|
||||
notificationRingLayer.strokeColor = resolved.cgColor
|
||||
notificationRingLayer.shadowColor = resolved.cgColor
|
||||
flashLayer.strokeColor = resolved.cgColor
|
||||
flashLayer.shadowColor = resolved.cgColor
|
||||
CATransaction.commit()
|
||||
}
|
||||
|
||||
func setBackgroundColor(_ color: NSColor) {
|
||||
guard let layer = backgroundView.layer else { return }
|
||||
CATransaction.begin()
|
||||
|
|
@ -4170,6 +4191,7 @@ final class GhosttySurfaceScrollView: NSView {
|
|||
return
|
||||
}
|
||||
|
||||
setFocusFlashColor(nil)
|
||||
CATransaction.begin()
|
||||
CATransaction.setDisableActions(true)
|
||||
notificationRingOverlayView.isHidden = !visible
|
||||
|
|
@ -4392,7 +4414,7 @@ final class GhosttySurfaceScrollView: NSView {
|
|||
}
|
||||
#endif
|
||||
|
||||
func triggerFlash() {
|
||||
func triggerFlash(color: NSColor? = nil) {
|
||||
DispatchQueue.main.async { [weak self] in
|
||||
guard let self else { return }
|
||||
#if DEBUG
|
||||
|
|
@ -4400,6 +4422,7 @@ final class GhosttySurfaceScrollView: NSView {
|
|||
Self.recordFlash(for: surfaceId)
|
||||
}
|
||||
#endif
|
||||
self.setFocusFlashColor(color)
|
||||
self.updateFlashPath()
|
||||
self.flashLayer.removeAllAnimations()
|
||||
self.flashLayer.opacity = 0
|
||||
|
|
|
|||
|
|
@ -1313,6 +1313,7 @@ final class BrowserPanel: Panel, ObservableObject {
|
|||
|
||||
/// Increment to request a UI-only flash highlight (e.g. from a keyboard shortcut).
|
||||
@Published private(set) var focusFlashToken: Int = 0
|
||||
@Published private(set) var focusFlashColor: NSColor = cmuxAccentNSColor()
|
||||
|
||||
/// Sticky omnibar-focus intent. This survives view mount timing races and is
|
||||
/// cleared only after BrowserPanelView acknowledges handling it.
|
||||
|
|
@ -1520,7 +1521,8 @@ final class BrowserPanel: Panel, ObservableObject {
|
|||
workspaceId = newWorkspaceId
|
||||
}
|
||||
|
||||
func triggerFlash() {
|
||||
func triggerFlash(color: NSColor? = nil) {
|
||||
focusFlashColor = color ?? cmuxAccentNSColor()
|
||||
focusFlashToken &+= 1
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -296,6 +296,10 @@ struct BrowserPanelView: View {
|
|||
)
|
||||
}
|
||||
|
||||
private var focusFlashColor: Color {
|
||||
Color(nsColor: panel.focusFlashColor)
|
||||
}
|
||||
|
||||
var body: some View {
|
||||
VStack(spacing: 0) {
|
||||
addressBar
|
||||
|
|
@ -303,8 +307,8 @@ struct BrowserPanelView: View {
|
|||
}
|
||||
.overlay {
|
||||
RoundedRectangle(cornerRadius: FocusFlashPattern.ringCornerRadius)
|
||||
.stroke(cmuxAccentColor().opacity(focusFlashOpacity), lineWidth: 3)
|
||||
.shadow(color: cmuxAccentColor().opacity(focusFlashOpacity * 0.35), radius: 10)
|
||||
.stroke(focusFlashColor.opacity(focusFlashOpacity), lineWidth: 3)
|
||||
.shadow(color: focusFlashColor.opacity(focusFlashOpacity * 0.35), radius: 10)
|
||||
.padding(FocusFlashPattern.ringInset)
|
||||
.allowsHitTesting(false)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,5 +1,6 @@
|
|||
import Foundation
|
||||
import Combine
|
||||
import AppKit
|
||||
|
||||
/// Type of panel content
|
||||
public enum PanelType: String, Codable, Sendable {
|
||||
|
|
@ -70,7 +71,8 @@ public protocol Panel: AnyObject, Identifiable, ObservableObject where ID == UUI
|
|||
func unfocus()
|
||||
|
||||
/// Trigger a focus flash animation for this panel.
|
||||
func triggerFlash()
|
||||
/// - Parameter color: Optional override color for this flash.
|
||||
func triggerFlash(color: NSColor?)
|
||||
}
|
||||
|
||||
/// Extension providing default implementations
|
||||
|
|
|
|||
|
|
@ -166,8 +166,8 @@ final class TerminalPanel: Panel, ObservableObject {
|
|||
surface.needsConfirmClose()
|
||||
}
|
||||
|
||||
func triggerFlash() {
|
||||
hostedView.triggerFlash()
|
||||
func triggerFlash(color: NSColor? = nil) {
|
||||
hostedView.triggerFlash(color: color)
|
||||
}
|
||||
|
||||
func applyWindowBackgroundIfActive() {
|
||||
|
|
|
|||
|
|
@ -2934,8 +2934,23 @@ final class Workspace: Identifiable, ObservableObject {
|
|||
|
||||
// MARK: - Flash/Notification Support
|
||||
|
||||
nonisolated static func resolvedFocusFlashColor(
|
||||
customColorHex: String?,
|
||||
fallback: NSColor = cmuxAccentNSColor()
|
||||
) -> NSColor {
|
||||
guard let normalizedHex = customColorHex.flatMap(WorkspaceTabColorSettings.normalizedHex),
|
||||
let color = NSColor(hex: normalizedHex) else {
|
||||
return fallback
|
||||
}
|
||||
return color
|
||||
}
|
||||
|
||||
private var focusFlashColor: NSColor {
|
||||
Self.resolvedFocusFlashColor(customColorHex: customColor)
|
||||
}
|
||||
|
||||
func triggerFocusFlash(panelId: UUID) {
|
||||
panels[panelId]?.triggerFlash()
|
||||
panels[panelId]?.triggerFlash(color: focusFlashColor)
|
||||
}
|
||||
|
||||
func triggerNotificationFocusFlash(
|
||||
|
|
@ -2951,7 +2966,7 @@ final class Workspace: Identifiable, ObservableObject {
|
|||
if requiresSplit && !isSplit {
|
||||
return
|
||||
}
|
||||
terminalPanel.triggerFlash()
|
||||
terminalPanel.triggerFlash(color: focusFlashColor)
|
||||
}
|
||||
|
||||
func triggerDebugFlash(panelId: UUID) {
|
||||
|
|
|
|||
|
|
@ -2848,6 +2848,26 @@ final class WorkspaceTabColorSettingsTests: XCTestCase {
|
|||
XCTAssertNotEqual(rendered.hexString(), originalHex)
|
||||
XCTAssertGreaterThan(rendered.luminance, base.luminance)
|
||||
}
|
||||
|
||||
func testWorkspaceFocusFlashColorUsesCustomHexWhenValid() {
|
||||
let fallback = NSColor(hex: "#112233")!
|
||||
let resolved = Workspace.resolvedFocusFlashColor(
|
||||
customColorHex: " c0392b ",
|
||||
fallback: fallback
|
||||
)
|
||||
|
||||
XCTAssertEqual(resolved.hexString(), "#C0392B")
|
||||
}
|
||||
|
||||
func testWorkspaceFocusFlashColorFallsBackWhenHexIsInvalid() {
|
||||
let fallback = NSColor(hex: "#112233")!
|
||||
let resolved = Workspace.resolvedFocusFlashColor(
|
||||
customColorHex: "not-a-color",
|
||||
fallback: fallback
|
||||
)
|
||||
|
||||
XCTAssertEqual(resolved.hexString(), "#112233")
|
||||
}
|
||||
}
|
||||
|
||||
final class WorkspaceAutoReorderSettingsTests: XCTestCase {
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue