Extract the exercise info view and ready the figure palette and app icon for macOS
This commit is contained in:
@@ -283,136 +283,3 @@ private struct MachineSettingsCard: View {
|
||||
}
|
||||
}
|
||||
|
||||
/// Renders a parsed `ExerciseInfo`: summary, target chips, then each authored
|
||||
/// section with its numbered steps or bullets. Internal so the debug figure-review
|
||||
/// screen can reuse it.
|
||||
struct ExerciseInfoContent: View {
|
||||
let info: ExerciseInfo
|
||||
|
||||
var body: some View {
|
||||
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))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// 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
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user