import SwiftUI /// The week as a time grid: one column per day, a coloured block per lesson. struct WeekGrid: View { @Environment(AppModel.self) private var model var timetable: Timetable var monday: CivilDate var onOpen: (LessonRef) -> Void private let hour: CGFloat = 60 private let gutter: CGFloat = 32 var body: some View { let all = (0..<7).map { monday.adding(days: $0) }.map { ($0, model.slots(timetable, on: $0)) } // Sunday only takes a column when something happens on it. let days = all.filter { $0.0.weekday < 7 || !$0.1.isEmpty } let slots = days.flatMap(\.1) let first = min(8, slots.map(\.start.hour).min() ?? 8) let last = max(18, slots.map { $0.end.hour + ($0.end.minute > 0 ? 1 : 0) }.max() ?? 18) let hours = last - first let today = model.today VStack(spacing: 0) { HStack(spacing: 0) { Color.clear.frame(width: gutter, height: 1) ForEach(days, id: \.0) { day in let d = day.0 let isToday = d == today VStack(spacing: 2) { Text(d.shortWeekday).font(.caption.weight(.medium)).foregroundStyle(isToday ? Color.brand : .secondary) Text("\(d.day)") .font(.headline) .foregroundStyle(isToday ? Color.onBrand : .primary) .frame(width: 34, height: 34) .background(isToday ? Color.brand : .clear, in: .circle) } .frame(maxWidth: .infinity) .accessibilityElement(children: .ignore) .accessibilityLabel(d.weekdayName + ", " + d.longLabel) } } .padding(.bottom, 8) ScrollView { HStack(alignment: .top, spacing: 0) { ZStack(alignment: .topLeading) { ForEach(first.. CGFloat { hour * CGFloat(t.minutes - first * 60) / 60 } private func column(_ d: CivilDate, _ daySlots: [Slot], first: Int, hours: Int, today: CivilDate) -> some View { GeometryReader { geo in let width = geo.size.width ZStack(alignment: .topLeading) { Path { p in for h in 0...hours { p.move(to: CGPoint(x: 0, y: hour * CGFloat(h))) p.addLine(to: CGPoint(x: width, y: hour * CGFloat(h))) } p.move(to: .zero) p.addLine(to: CGPoint(x: 0, y: hour * CGFloat(hours))) } .stroke(Color(.separator), lineWidth: 0.5) ForEach(daySlots) { slot in let top = y(slot.start, first) let height = y(slot.end, first) - top let w = width / CGFloat(slot.lessons.count) ForEach(Array(slot.lessons.enumerated()), id: \.element.key) { i, lesson in Button { onOpen(LessonRef(lesson: lesson, date: d)) } label: { LessonBlock(lesson: lesson, wide: w >= 96, notes: model.notes.forLesson(lesson.subject, d, lesson.start).open.count) } .buttonStyle(.plain) .frame(width: max(0, w - 4), height: max(0, height - 2)) .offset(x: w * CGFloat(i) + 2, y: top + 1) } } if d == today, (first..<(first + hours)).contains(model.clock.hour) { Capsule().fill(Color.brand).frame(height: 2).offset(y: y(model.clock, first) - 1) } } } .frame(height: hour * CGFloat(hours)) .frame(maxWidth: .infinity) } } /// A lesson in the grid: its abbreviation (the full name on wide columns) and room. private struct LessonBlock: View { @Environment(\.display) private var show var lesson: Lesson var wide: Bool var notes: Int var body: some View { WithSubjectColor(subject: lesson.subject) { c in VStack(alignment: .leading, spacing: 1) { Text(wide ? lesson.subject : abbreviate(lesson.subject)) .font(.caption.weight(.semibold)) .lineLimit(wide ? 3 : 2) if show.rooms, let room = lesson.distance ? "дист." : lesson.rooms.first { Text(room).font(.caption2).lineLimit(1).opacity(0.9) } Spacer(minLength: 0) if notes > 0 { Image(systemName: "checklist").font(.caption2.weight(.bold)) } } .foregroundStyle(c.onStrong) .padding(4) .frame(maxWidth: .infinity, maxHeight: .infinity, alignment: .topLeading) .background(c.strong, in: .rect(cornerRadius: 8, style: .continuous)) } .accessibilityElement(children: .ignore) .accessibilityLabel("\(lesson.subject), \(lesson.start)" + (lesson.rooms.isEmpty ? "" : ", ауд. " + lesson.rooms.joined(separator: ", "))) } }