// // ExerciseLibraryView.swift // Workouts // // Copyright 2026 Rouslan Zenetl. All Rights Reserved. // import SwiftUI /// The Exercises library screen, pushed from Settings → Library: every exercise with /// a bundled motion rig (the same catalog the picker offers), each opening its /// reference page. struct ExerciseLibraryView: View { var body: some View { List(ExerciseMotionLibrary.exerciseNames, id: \.self) { name in NavigationLink(name) { ExerciseLibraryDetailView(exerciseName: name) } } .navigationTitle("Exercises") } } /// One library exercise, in the standard half-and-half exercise layout: the authored /// reference content (summary, targets, setup/execution/cues…) scrolls in the top /// half, the looping form-guide figure holds the bottom. Falls back to a full-screen /// figure when no `info.md` is bundled. struct ExerciseLibraryDetailView: View { let exerciseName: String private var info: ExerciseInfo? { ExerciseInfoLibrary.info(for: exerciseName) } var body: some View { VStack(spacing: 0) { if let info { ExerciseInfoContent(info: info) } ExerciseFigureSlot(exerciseName: exerciseName) } .navigationTitle(exerciseName) .navigationBarTitleDisplayMode(.inline) } } /// Renders a parsed `ExerciseInfo`: summary, target chips, then each authored /// section with its numbered steps or bullets. private struct ExerciseInfoContent: View { let info: ExerciseInfo var body: some View { ScrollView { VStack(alignment: .leading, spacing: 16) { if !info.summary.isEmpty { Text(info.summary) .font(.callout) .foregroundStyle(.secondary) } if !info.targets.isEmpty { TargetChips(targets: info.targets) } ForEach(info.sections, id: \.title) { section in VStack(alignment: .leading, spacing: 6) { Text(section.title) .font(.headline) ForEach(Array(section.items.enumerated()), id: \.offset) { index, item in itemRow(item, ordinal: ordinal(for: item, at: index, in: section)) } } } } .padding() .frame(maxWidth: .infinity, alignment: .leading) } .frame(maxWidth: .infinity, maxHeight: .infinity) } /// Steps number themselves by their position among the section's steps, so an /// interleaved paragraph doesn't break the count. private func ordinal(for item: ExerciseInfo.Item, at index: Int, in section: ExerciseInfo.Section) -> Int? { guard case .step = item else { return nil } return section.items[...index].reduce(0) { count, it in if case .step = it { count + 1 } else { count } } } @ViewBuilder private func itemRow(_ item: ExerciseInfo.Item, ordinal: Int?) -> some View { switch item { case .step(let text): HStack(alignment: .firstTextBaseline, spacing: 6) { Text("\(ordinal ?? 0).") .foregroundStyle(.secondary) .monospacedDigit() Text(text) } .font(.callout) case .bullet(let text): HStack(alignment: .firstTextBaseline, spacing: 6) { Text("•") .foregroundStyle(.secondary) Text(text) } .font(.callout) case .paragraph(let text): Text(text) .font(.callout) } } } /// The `Targets:` muscles as wrapping capsule chips. private struct TargetChips: View { let targets: [String] var body: some View { FlowLayout(spacing: 6) { ForEach(targets, id: \.self) { target in Text(target) .font(.caption) .padding(.horizontal, 10) .padding(.vertical, 4) .background(Color.accentColor.opacity(0.12), in: Capsule()) .foregroundStyle(Color.accentColor) } } } } /// Minimal leading-aligned wrapping layout for the target chips. private struct FlowLayout: Layout { var spacing: CGFloat = 6 func sizeThatFits(proposal: ProposedViewSize, subviews: Subviews, cache: inout ()) -> CGSize { let rows = layoutRows(maxWidth: proposal.width ?? .infinity, subviews: subviews) let height = rows.map(\.height).reduce(0, +) + spacing * CGFloat(max(0, rows.count - 1)) return CGSize(width: proposal.width ?? rows.map(\.width).max() ?? 0, height: height) } func placeSubviews(in bounds: CGRect, proposal: ProposedViewSize, subviews: Subviews, cache: inout ()) { var y = bounds.minY var index = 0 for row in layoutRows(maxWidth: bounds.width, subviews: subviews) { var x = bounds.minX for size in row.sizes { subviews[index].place( at: CGPoint(x: x, y: y), proposal: ProposedViewSize(size) ) x += size.width + spacing index += 1 } y += row.height + spacing } } private struct Row { var sizes: [CGSize] = [] var width: CGFloat = 0 var height: CGFloat = 0 } private func layoutRows(maxWidth: CGFloat, subviews: Subviews) -> [Row] { var rows: [Row] = [] var current = Row() for subview in subviews { let size = subview.sizeThatFits(.unspecified) let next = current.sizes.isEmpty ? size.width : current.width + spacing + size.width if next > maxWidth, !current.sizes.isEmpty { rows.append(current) current = Row() } current.sizes.append(size) current.width = current.sizes.isEmpty ? 0 : current.width + (current.sizes.count == 1 ? size.width : spacing + size.width) current.height = max(current.height, size.height) } if !current.sizes.isEmpty { rows.append(current) } return rows } }