import Frida import LumaCore import SwiftUI struct PatternsSidebarRows: View { let engine: Engine @Binding var selection: SidebarItemID? @State private var outlines: [String: [PatternTypeSummary]] = [:] @State private var expanded: Set = [] @AppStorage("sidebar.patterns.expansion") private var expansion: SidebarExpansion = .expanded private var sources: [PatternSource] { engine.patterns.sources } var body: some View { SidebarPatternsRow(count: sources.count, isExpanded: expansion == .expanded) { expansion = expansion == .expanded ? .collapsed : .expanded } .tag(SidebarItemID.patterns) ForEach(expansion == .expanded ? sources : []) { source in let types = outlines[source.id] ?? [] let isExpanded = expanded.contains(source.id) SidebarPatternRow( source: source, engine: engine, selection: $selection, hasTypes: !types.isEmpty, isExpanded: isExpanded, onToggleExpansion: { toggle(source.id) } ) .tag(SidebarItemID.pattern(source.id)) if isExpanded { PatternTypeSidebarChildren(source: source, types: types, selection: $selection) } } .task(id: sources) { await outline() } .onChange(of: selection, initial: true) { if let selectedSource { expansion = .expanded expanded.insert(selectedSource) } } } private var selectedSource: String? { switch selection { case .pattern(let id), .patternType(let id, _): return id default: return nil } } private func toggle(_ sourceID: String) { if expanded.contains(sourceID) { expanded.remove(sourceID) } else { expanded.insert(sourceID) } } private func outline() async { var outlined: [String: [PatternTypeSummary]] = [:] for source in sources { guard let summary = try? await engine.patternDecoder.summary(of: source) else { continue } outlined[source.id] = summary.declaredTypes } guard !Task.isCancelled else { return } outlines = outlined expanded.formIntersection(sources.map(\.id)) } } private struct PatternTypeSidebarChildren: View { let source: PatternSource let types: [PatternTypeSummary] @Binding var selection: SidebarItemID? var body: some View { let highlights = types.sidebarHighlights(selectedID: selectedTypeName) ForEach(highlights) { type in SidebarFeatureRowLabel( icon: { Image(systemName: type.kind.symbolName).font(.system(size: 11)) }, title: type.name, help: type.kind.title ) .tag(SidebarItemID.patternType(source.id, type.name)) } if types.count > highlights.count { SidebarBrowseAllRow(count: types.count) { dismiss in SidebarBrowserPopover( placeholder: "Filter types", emptyMessage: "No matching types", items: types, groupName: { $0.kind.title }, title: { $0.name }, help: { $0.kind.title }, isDimmed: { _ in false }, matches: { type, query in type.name.localizedCaseInsensitiveContains(query) }, onChoose: { type in selection = .patternType(source.id, type.name) dismiss() } ) } } } private var selectedTypeName: String? { if case .patternType(let id, let name) = selection, id == source.id { return name } return nil } } private struct SidebarPatternsRow: View { let count: Int let isExpanded: Bool let onToggle: () -> Void var body: some View { HStack(spacing: 8) { Image(systemName: "square.stack.3d.up") .frame(width: 18, alignment: .center) Text("Patterns") Spacer() if count > 0 { Text("\(count)").font(.caption).foregroundStyle(.secondary) Button(action: onToggle) { Image(systemName: "chevron.right") .font(.system(size: 10, weight: .semibold)) .foregroundStyle(.secondary) .rotationEffect(.degrees(isExpanded ? 90 : 0)) .frame(width: sidebarChevronWidth, height: sidebarChevronWidth) .contentShape(Rectangle()) } .buttonStyle(.plain) .accessibilityLabel(isExpanded ? "Collapse patterns" : "Expand patterns") } } .accessibilityElement(children: .combine) .accessibilityIdentifier("sidebar.patterns") } } private struct SidebarPatternRow: View { let source: PatternSource let engine: Engine @Binding var selection: SidebarItemID? let hasTypes: Bool let isExpanded: Bool let onToggleExpansion: () -> Void @State private var isShowingRename = false @State private var newName = "" @State private var isShowingDeleteConfirmation = false @State private var errorMessage: String? var body: some View { HStack(spacing: 0) { SidebarDisclosure(isExpanded: isExpanded, canToggle: hasTypes, onToggle: onToggleExpansion) Image(systemName: source.kind.symbolName) .foregroundStyle(.secondary) .frame(width: sidebarChildIconWidth) .padding(.trailing, sidebarIconToLabelSpacing) Text(source.name) .lineLimit(1) .truncationMode(.tail) Spacer(minLength: 0) } .accessibilityElement(children: .combine) .accessibilityIdentifier("sidebar.pattern.\(source.id)") .contextMenu { Button { newName = source.name isShowingRename = true } label: { Label("Rename…", systemImage: "pencil") } Button(role: .destructive) { isShowingDeleteConfirmation = true } label: { Label("Delete", systemImage: "trash") } } .alert("Rename \(source.kind.title)", isPresented: $isShowingRename) { TextField("Name", text: $newName) Button("Rename") { rename() } Button("Cancel", role: .cancel) {} } .confirmationDialog("Delete \(source.name)?", isPresented: $isShowingDeleteConfirmation, titleVisibility: .visible) { Button("Delete", role: .destructive) { delete() } Button("Cancel", role: .cancel) {} } message: { Text("This removes \(source.id) from the pattern library.") } .alert("Pattern library error", isPresented: errorBinding, presenting: errorMessage) { _ in Button("OK") { errorMessage = nil } } message: { message in Text(message) } } private func rename() { do { let renamed = try engine.patterns.rename(source.id, to: newName) if selection == .pattern(source.id) { selection = .pattern(renamed.id) } } catch { errorMessage = error.localizedDescription } } private func delete() { do { try engine.patterns.delete(source.id) if selection == .pattern(source.id) { selection = .patterns } } catch { errorMessage = error.localizedDescription } } private var errorBinding: Binding { Binding( get: { errorMessage != nil }, set: { if !$0 { errorMessage = nil } } ) } } extension PatternSource.Kind { var title: String { switch self { case .pattern: return "Pattern" case .library: return "Library" } } var symbolName: String { switch self { case .pattern: return "doc.text" case .library: return "books.vertical" } } } extension PatternTypeKind { var title: String { switch self { case .struct: return "Struct" case .union: return "Union" case .enum: return "Enum" case .bitfield: return "Bitfield" case .alias: return "Alias" } } var symbolName: String { switch self { case .struct, .union: return "curlybraces" case .enum: return "list.number" case .bitfield: return "01.square" case .alias: return "equal" } } }