diff --git a/.claude/rules/theme.md b/.claude/rules/theme.md index 5616bb0a70..6829ac0356 100644 --- a/.claude/rules/theme.md +++ b/.claude/rules/theme.md @@ -14,6 +14,7 @@ paths: - **Chrome never reads a slot.** It asks `ThemeBackdropState.isThemed(in:scope:)` or `isThemed(highContrast:scope:)` whether the backdrop behind it is themed, and answers with a system style: - A bar over a pane takes `.paneBarBackground(_:ignoresSafeAreaEdges:)` with the native style it drew before (`.bar` by default, or a flat `controlBackgroundColor` or `windowBackgroundColor`). It draws that style while the backdrop is unthemed and nothing while it is themed, because a material over a themed pane reads as a system band. No edges by default, and a top bar keeps that default, so it never reaches under the toolbar. - A bottom status bar takes `.statusBarChrome()`. `StatusBarChrome` asks `ThemeBackdropState` itself and drops its material over a themed backdrop, so a host sets nothing. + - A list or scroll view filling a glass sidebar or inspector item takes `.splitItemGlassBackground()`. From macOS 26 it hides the list's own content-background material, which never follows the window color, so the item's glass shows through and the pane matches the sidebar; before macOS 26 the list keeps its background. The inspector field list and the agent's proposed statements take it. - The editor tab strip track draws `secondarySystemFill` over a themed backdrop on macOS 26 and later, where the track sits on the theme's window color, and its unthemed fill otherwise. Never a palette color. - **A code field takes the editor theme wherever it appears.** Startup Commands, Pre-Connect Script, a favorite's query, Rewind, Agent SQL, the deep-link Startup SQL and the tunnel command preview wear `editor.*`; the form or sheet around them stays system. - **The slot family follows what the surface shows.** Code, values, value trees and wells: `editor.*`, with tree tones from `editor.syntax.*`. Records, schema objects, diagram nodes, chart plots and states that stand in for a grid: `dataGrid.*`. Diff rows: `dataGrid.inserted`, `modified` and `deleted` over the editor background and text. A region behind panes: `window.background`. Identity, severity and status hues stay system. @@ -52,7 +53,7 @@ paths: - **The four content windows are `ContentWindow`s**: main (`TabWindowController`), Data File, Compare & Sync and the value viewers. Each holds a `ThemeWindowBackdrop`, which on macOS 26 and later sets `NSWindow.backgroundColor` to `window.background` only while the theme sets it and puts the window's own color back when it stops; the sidebar, inspector and toolbar glass take their tone from it. Before macOS 26 the window keeps its own color. `ThemeWindowBackdrop.canReachChrome` is that gate; the titlebar policy, `DetailPaneBackdrop.extendsUnderToolbar` and `EditorTabStripPalette.followsWindowBackdrop` read it rather than an `#available` of their own. - **One titlebar policy, in `ContentWindow`.** On macOS 26 and later the titlebar is transparent, so the backdrop reaches the toolbar; in full screen with the toolbar hidden it keeps its background, since it then slides down over the content. Before macOS 26 it stays opaque: a transparent titlebar there swaps its material for a flat fill, which fills chrome. A new content window subclasses `ContentWindow`. Toolbar separators stay each window's own. - **The detail column sits on `window.background` on every macOS version.** `MainSplitViewController` installs `DetailColumnBackdrop` under the detail panes through `WorkspacePaneHost.installBackdrop(_:)`, so Connecting, Unavailable, the agent and every other state get it. Before macOS 26 it stops below the toolbar, which keeps the system color. The backdrop passes clicks through. `DetailPaneBackdropFill` goes only there; a pane that draws nothing shows it. The Compare & Sync and Data File roots fill with `.themeBackground(.windowBackdrop, native:)`. -- **Never fill chrome.** No fill on the sidebar, inspector frame, toolbar or titlebar, and no `.tint` with a theme color. AppKit keeps its own selection unless the theme sets the selection slot. SwiftLint's `pane_hidden_background` bans `.scrollContentBackground(.hidden)` and `.listRowBackground(` in app code, and `window_background_outside_backdrop` bans a window color write outside `ThemeWindowBackdrop`. +- **Never fill chrome.** No fill on the sidebar, inspector frame, toolbar or titlebar, and no `.tint` with a theme color. AppKit keeps its own selection unless the theme sets the selection slot. SwiftLint's `pane_hidden_background` bans `.scrollContentBackground(.hidden)` and `.listRowBackground(` in app code outside `SplitItemGlassBackground`, and `window_background_outside_backdrop` bans a window color write outside `ThemeWindowBackdrop`. ## Engine and catalog diff --git a/apple/.swiftlint.yml b/apple/.swiftlint.yml index d1d0cb0a95..b8713e4a97 100644 --- a/apple/.swiftlint.yml +++ b/apple/.swiftlint.yml @@ -397,9 +397,9 @@ custom_rules: pane_hidden_background: name: "Pane Keeps Its Background" regex: '\.scrollContentBackground\(\s*\.hidden\s*\)|\b[Tt]hemedContent|\.listRowBackground\(' - excluded: ['.*/(Packages|Plugins)/'] + excluded: ['.*/(Packages|Plugins)/', '.*/TablePro/Theme/SplitItemGlassBackground\.swift$'] excluded_match_kinds: [comment, doccomment] - message: "Let the List, Form or ScrollView draw its own background and rows." + message: "Let the List, Form or ScrollView draw its own background and rows. Inside a glass sidebar or inspector, use .splitItemGlassBackground()." severity: error window_background_outside_backdrop: name: "Window Background Outside The Backdrop" diff --git a/apple/TablePro/Theme/SplitItemGlassBackground.swift b/apple/TablePro/Theme/SplitItemGlassBackground.swift new file mode 100644 index 0000000000..a41b6ee3f1 --- /dev/null +++ b/apple/TablePro/Theme/SplitItemGlassBackground.swift @@ -0,0 +1,26 @@ +import SwiftUI + +/// From macOS 26 a sidebar or inspector split item is glass, which takes its tone from the window +/// color. A `List` puts its own content-background material over that glass, and that material +/// never follows the window color, so it reads as a second surface inside the pane. Apple's +/// guidance for the new design is to remove such effect views so the glass shows through. +/// +/// The condition is fixed for the process: SwiftUI never puts the material back when a list's +/// scroll background goes from hidden to automatic at runtime, measured. +private struct SplitItemGlassBackground: ViewModifier { + @ViewBuilder + func body(content: Content) -> some View { + if ThemeWindowBackdrop.canReachChrome { + content.scrollContentBackground(.hidden) + } else { + content + } + } +} + +internal extension View { + /// For a list or scroll view filling a sidebar or inspector split item. + func splitItemGlassBackground() -> some View { + modifier(SplitItemGlassBackground()) + } +} diff --git a/apple/TablePro/Views/Agent/AgentProposedStatementsView.swift b/apple/TablePro/Views/Agent/AgentProposedStatementsView.swift index 5eeb8644ff..df07fbb4e1 100644 --- a/apple/TablePro/Views/Agent/AgentProposedStatementsView.swift +++ b/apple/TablePro/Views/Agent/AgentProposedStatementsView.swift @@ -32,6 +32,7 @@ internal struct AgentProposedStatementsView: View { } } .listStyle(.inset) + .splitItemGlassBackground() } } } diff --git a/apple/TablePro/Views/RowInspector/InspectorFieldListView.swift b/apple/TablePro/Views/RowInspector/InspectorFieldListView.swift index a5838e308e..ab6c8b4c17 100644 --- a/apple/TablePro/Views/RowInspector/InspectorFieldListView.swift +++ b/apple/TablePro/Views/RowInspector/InspectorFieldListView.swift @@ -136,6 +136,7 @@ internal struct InspectorFieldListView: View { /// `listRowInsets` is consulted, so the pane's content sat at 24pt while its own header sat /// at 10. `.plain` lands at half of `intercellSpacing` and can be corrected onto the edge. .listStyle(.plain) + .splitItemGlassBackground() .modifier(InspectorFieldKeyShortcuts( moveFocus: { forward in moveFocus(within: fields, forward: forward) }, applyStateShortcut: applyStateShortcut diff --git a/apple/TableProTests/Views/RowInspector/SplitItemGlassBackgroundTests.swift b/apple/TableProTests/Views/RowInspector/SplitItemGlassBackgroundTests.swift new file mode 100644 index 0000000000..e70632b873 --- /dev/null +++ b/apple/TableProTests/Views/RowInspector/SplitItemGlassBackgroundTests.swift @@ -0,0 +1,55 @@ +import AppKit +import SwiftUI +@testable import TablePro +import Testing + +// A list's content-background material never takes the window color, so inside the glass +// inspector it drew a second, untinted surface behind the fields. +@MainActor +struct SplitItemGlassBackgroundTests { + private static let size = NSSize(width: 280, height: 360) + + private static func contentBackgrounds(in view: NSView?) -> [NSVisualEffectView] { + guard let view else { return [] } + let own = (view as? NSVisualEffectView).map { $0.material == .contentBackground ? [$0] : [] } ?? [] + return own + view.subviews.flatMap { contentBackgrounds(in: $0) } + } + + private func drawsListMaterial(_ view: some View) -> Bool { + let window = ThemeTestFixture.host(view, size: Self.size) + defer { window.orderOut(nil) } + return !Self.contentBackgrounds(in: window.contentView).isEmpty + } + + private func fieldList() -> InspectorFieldListView { + let state = MultiRowEditState() + state.configure( + selectedRowIndices: [0], + rowIDs: [.existing(0)], + allRows: [["1", "Still"]], + absentCells: [[]], + columns: ["id", "name"], + columnTypes: [.integer(rawType: "INTEGER"), .text(rawType: "TEXT")], + externallyModifiedColumns: [] + ) + return InspectorFieldListView(editState: state, isEditable: true, databaseType: .sqlite, userDefinedTypeScope: nil) + } + + @Test("A plain list keeps its material where split items are not glass and drops it where they are") + func modifierFollowsTheGlassRelease() { + let list = List { Text(verbatim: "row") }.listStyle(.plain) + #expect(drawsListMaterial(list)) + #expect(drawsListMaterial(list.splitItemGlassBackground()) == !ThemeWindowBackdrop.canReachChrome) + } + + @Test("The inspector's field list lets the inspector glass show through on macOS 26 and later") + func inspectorFieldListShowsTheGlass() { + #expect(drawsListMaterial(fieldList()) == !ThemeWindowBackdrop.canReachChrome) + } + + @Test("The agent's proposed statements let the trailing pane's glass show through on macOS 26 and later") + func proposedStatementsShowTheGlass() { + let statements = [ProposedStatement(id: "1", sql: "SELECT 1", toolName: "run_query", state: .ran)] + #expect(drawsListMaterial(AgentProposedStatementsView(statements: statements)) == !ThemeWindowBackdrop.canReachChrome) + } +} diff --git a/apple/changelog.d/3590.fixed.md b/apple/changelog.d/3590.fixed.md new file mode 100644 index 0000000000..bdd707b701 --- /dev/null +++ b/apple/changelog.d/3590.fixed.md @@ -0,0 +1 @@ +The row inspector's field area drew a gray surface that ignored the theme instead of the inspector's own background.