Refine Sidebar glass and selection states

This commit is contained in:
Kaede Akino committed 2026-09-23 12:19:32 +08:00
1 parent 947c0dc2ab
commit fddccff4ee
4 files changed
+100 -109

No files matched your search

+12 -7
View File
@@ -481,9 +481,9 @@ struct LegacySidebarMaterialBackground: NSViewRepresentable {
}
}
/// A native-feeling emphasized selection: the key window uses an accent fill
/// with reversed content, while an inactive window keeps the same destination
/// legible with a quiet accent wash and accent-colored content.
/// A native selection: the key window uses the system selection color with
/// reversed content; an inactive window uses the system's neutral selection
/// background and unemphasized text color.
///
/// The row's context menu is drawn here rather than by the caller, and it is a
/// native menu rather than SwiftUI's: `contextMenu` derives its source rectangle
@@ -554,9 +554,7 @@ struct TermindSidebarRowBackground: ViewModifier {
private var fillColor: Color {
if isSelected {
controlActiveState == .key
? TermindColor.accent
: TermindColor.accent.opacity(0.14)
SidebarSelectionStyle.background(controlActiveState: controlActiveState)
} else {
Highlight.fill(isSelected: false, isHovering: isHovering)
}
@@ -564,6 +562,13 @@ struct TermindSidebarRowBackground: ViewModifier {
}
enum SidebarSelectionStyle {
static func background(controlActiveState: ControlActiveState) -> Color {
Color(
nsColor: controlActiveState == .key
? .selectedContentBackgroundColor : .unemphasizedSelectedContentBackgroundColor
)
}
static func foreground(
isSelected: Bool,
controlActiveState: ControlActiveState,
@@ -572,7 +577,7 @@ enum SidebarSelectionStyle {
guard isSelected else { return unselected }
return controlActiveState == .key
? AnyShapeStyle(TermindColor.selectedForeground)
: AnyShapeStyle(.tint)
: AnyShapeStyle(Color(nsColor: .unemphasizedSelectedTextColor))
}
}
+56 -47
View File
@@ -57,7 +57,7 @@ enum AssistantConversationMenuSurface {
/// every mode of the column ends at the same line. Its two controls are shaped
/// by what they are: New Chat is a capsule because it is a labelled action that
/// takes the width, and Archived is a circle because it is one glyph that
/// toggles — the same distinction the mode rail at the top of the column makes.
/// toggles. Both share neutral glass, including the archive's selected state.
struct AssistantSidebarView: View {
@Environment(WindowModel.self) private var window
@@ -71,54 +71,63 @@ struct AssistantSidebarView: View {
onOpen: { window.selectAssistant($0) }
)
HStack(spacing: Metrics.Space.s) {
Button {
window.openNewAssistantChat()
} label: {
Label("New Chat", systemImage: "plus.bubble")
.frame(
maxWidth: .infinity,
maxHeight: .infinity,
alignment: .center
)
.contentShape(.capsule)
}
.buttonStyle(.plain)
.termindGlassCapsule(isInteractive: true)
footer
.frame(height: Metrics.Sidebar.searchFieldHeight)
.padding(.horizontal, Metrics.Space.m)
.padding(.vertical, Metrics.Space.s)
}
}
Button {
window.showAssistantArchive()
} label: {
Image(systemName: "archivebox")
// Filled and reversed while it is the destination on
// screen, the same way a selected row states itself.
// An accent-on-glass glyph reads as "tinted", not as
// "this is where you are".
.foregroundStyle(
window.isAssistantArchivePresented
? AnyShapeStyle(TermindColor.selectedForeground)
: AnyShapeStyle(.primary)
)
.frame(
width: Metrics.Sidebar.searchFieldHeight,
height: Metrics.Sidebar.searchFieldHeight
)
.background {
if window.isAssistantArchivePresented {
Circle().fill(Color.accentColor)
}
}
.contentShape(.circle)
}
.buttonStyle(.plain)
.termindGlassCircle(isInteractive: true)
.help(Text("Archived"))
.accessibilityLabel(Text("Archived"))
.accessibilityAddTraits(window.isAssistantArchivePresented ? .isSelected : [])
@ViewBuilder
private var footer: some View {
if #available(macOS 26.0, *) {
GlassEffectContainer(spacing: Metrics.Space.s) {
footerButtons
}
.frame(height: Metrics.Sidebar.searchFieldHeight)
.padding(.horizontal, Metrics.Space.m)
.padding(.vertical, Metrics.Space.s)
} else {
footerButtons
}
}
private var footerButtons: some View {
HStack(spacing: Metrics.Space.s) {
Button {
window.openNewAssistantChat()
} label: {
Label("New Chat", systemImage: "plus.bubble")
.foregroundStyle(.primary)
.frame(
maxWidth: .infinity,
maxHeight: .infinity,
alignment: .center
)
.contentShape(.capsule)
}
.buttonStyle(.plain)
.termindGlassCapsule(isInteractive: true)
Button {
window.showAssistantArchive()
} label: {
Image(systemName: "archivebox")
.foregroundStyle(.primary)
.fontWeight(window.isAssistantArchivePresented ? .semibold : .regular)
.frame(
width: Metrics.Sidebar.searchFieldHeight,
height: Metrics.Sidebar.searchFieldHeight
)
.background {
if window.isAssistantArchivePresented {
Circle().fill(Color.primary.opacity(0.10))
}
}
.contentShape(.circle)
}
.buttonStyle(.plain)
.termindGlassCircle(isInteractive: true)
.help(Text("Archived"))
.accessibilityLabel(Text("Archived"))
.accessibilityAddTraits(window.isAssistantArchivePresented ? .isSelected : [])
}
}
}
+31 -54
View File
@@ -7,13 +7,15 @@ import TermindPresentation
import struct TermindDomain.Host
/// The window's one left-hand column. A glass mode rail switches the Navigator
/// and three Workspace tools in place without the square selection segments of
/// `NSSegmentedControl`.
/// The window's one left-hand column. Its mode buttons share one glass capsule
/// with a neutral selection highlight, keeping the symbols above the material.
struct WindowSidebarView: View {
@Environment(WindowModel.self) private var window
@Environment(\.accessibilityReduceMotion) private var reduceMotion
@Environment(\.controlActiveState) private var controlActiveState
@FocusState private var focusedMode: WindowSidebarState.Mode?
@Namespace private var modeSelectionNamespace
var body: some View {
VStack(spacing: 0) {
@@ -33,44 +35,20 @@ struct WindowSidebarView: View {
private var focusedPaneID: Pane.ID? { window.selectedWorkspace?.focusedPaneID }
@ViewBuilder
private var modeSelector: some View {
Group {
if #available(macOS 26.0, *) {
GlassEffectContainer(spacing: Metrics.Space.xs) {
modeButtons(usesSystemGlass: true)
}
.padding(Metrics.Space.xs)
.glassEffect(.regular, in: .capsule)
} else {
modeButtons(usesSystemGlass: false)
.padding(Metrics.Space.xs)
.termindGlassCapsule()
HStack(spacing: Metrics.Space.xs) {
ForEach(WindowSidebarState.Mode.allCases) { mode in
modeButton(mode)
}
}
.padding(Metrics.Space.xs)
.termindGlassCapsule()
.animation(reduceMotion ? nil : Motion.panel, value: window.sidebarMode)
.accessibilityElement(children: .contain)
.accessibilityLabel(Text("Sidebar"))
}
private func modeButtons(usesSystemGlass: Bool) -> some View {
HStack(spacing: Metrics.Space.xs) {
ForEach(Array(WindowSidebarState.Mode.allCases.enumerated()), id: \.element.id) {
index, mode in
if index > 0 {
Divider()
.frame(height: 18)
.opacity(0.55)
}
modeButton(mode, usesSystemGlass: usesSystemGlass)
}
}
}
private func modeButton(
_ mode: WindowSidebarState.Mode,
usesSystemGlass: Bool
) -> some View {
private func modeButton(_ mode: WindowSidebarState.Mode) -> some View {
let isSelected = window.sidebarMode == mode
let isEnabled = window.isSidebarModeEnabled(mode)
@@ -78,23 +56,27 @@ struct WindowSidebarView: View {
guard isEnabled else { return }
window.activateSidebarMode(mode)
} label: {
Group {
if #available(macOS 26.0, *), usesSystemGlass, isSelected {
modeIcon(mode, isSelected: true, isEnabled: isEnabled)
.background(Circle().fill(Color.accentColor))
.glassEffect(.clear.interactive(), in: .circle)
} else {
modeIcon(mode, isSelected: isSelected, isEnabled: isEnabled)
.background {
if isSelected {
Circle().fill(Color.accentColor)
}
}
modeIcon(mode, isSelected: isSelected, isEnabled: isEnabled)
.frame(maxWidth: .infinity)
.background {
if isSelected {
Capsule()
.fill(Color.primary.opacity(0.10))
.matchedGeometryEffect(id: "selection", in: modeSelectionNamespace)
}
}
}
.frame(maxWidth: .infinity)
.contentShape(.capsule)
}
.buttonStyle(.plain)
.focused($focusedMode, equals: mode)
.focusEffectDisabled()
.overlay {
if focusedMode == mode && controlActiveState == .key {
Capsule()
.strokeBorder(Color.primary.opacity(0.35), lineWidth: 2)
.allowsHitTesting(false)
}
}
.disabled(!isEnabled)
.help(Text(mode.label))
.accessibilityLabel(Text(mode.label))
@@ -107,14 +89,9 @@ struct WindowSidebarView: View {
isEnabled: Bool
) -> some View {
Image(systemName: mode.symbol)
.font(.system(size: 15, weight: .medium))
.foregroundStyle(
isSelected
? AnyShapeStyle(TermindColor.selectedForeground)
: isEnabled ? AnyShapeStyle(.primary) : AnyShapeStyle(.tertiary)
)
.font(.system(size: 15, weight: isSelected ? .semibold : .medium))
.foregroundStyle(isEnabled ? AnyShapeStyle(.primary) : AnyShapeStyle(.tertiary))
.frame(width: 36, height: 36)
.contentShape(.circle)
}
@ViewBuilder
+1 -1
View File
@@ -65,7 +65,7 @@ struct SidebarInlineRenameField: NSViewRepresentable {
guard isSelected else { return .labelColor }
return controlActiveState == .key
? .alternateSelectedControlTextColor
: .controlAccentColor
: .unemphasizedSelectedTextColor
}
private var placeholderColor: NSColor {