Refine Sidebar glass and selection states
This commit is contained in:
1 parent
947c0dc2ab
commit
fddccff4ee
4 files changed
+100
-109
No files matched your search
@@ -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))
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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 : [])
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -65,7 +65,7 @@ struct SidebarInlineRenameField: NSViewRepresentable {
|
||||
guard isSelected else { return .labelColor }
|
||||
return controlActiveState == .key
|
||||
? .alternateSelectedControlTextColor
|
||||
: .controlAccentColor
|
||||
: .unemphasizedSelectedTextColor
|
||||
}
|
||||
|
||||
private var placeholderColor: NSColor {
|
||||
|
||||
Reference in new issue
Block a user