Week view: render multi-day all-day events as continuous spanning bars
The all-day strip repeated a multi-day event once per day; now events span their start→end columns and pack into lanes (like the month view) via a GeometryReader layout. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -72,37 +72,66 @@ struct WeekView: View {
|
|||||||
|
|
||||||
// MARK: – All-day strip
|
// MARK: – All-day strip
|
||||||
|
|
||||||
private var allDayRow: some View {
|
private let allDayLaneHeight: CGFloat = 16
|
||||||
HStack(spacing: 0) {
|
|
||||||
Spacer().frame(width: timeColumnWidth)
|
/// All-day / multi-day events laid out as continuous bars: each event spans
|
||||||
ForEach(weekDays, id: \.self) { day in
|
/// its start→end columns (clamped to this week) and is packed into lanes so
|
||||||
let dayEvs = allDayEvents.filter { ev in
|
/// overlapping bars stack — mirrors the month view instead of repeating the
|
||||||
|
/// event once per day.
|
||||||
|
private func allDayBars() -> [(ev: CalEvent, start: Int, end: Int, lane: Int)] {
|
||||||
|
var spans: [(ev: CalEvent, start: Int, end: Int)] = []
|
||||||
|
for ev in allDayEvents {
|
||||||
|
var start: Int? = nil
|
||||||
|
var end = 0
|
||||||
|
for (i, day) in weekDays.enumerated() {
|
||||||
let ds = cal.startOfDay(for: day)
|
let ds = cal.startOfDay(for: day)
|
||||||
let de = cal.date(byAdding: .day, value: 1, to: ds)!
|
let de = cal.date(byAdding: .day, value: 1, to: ds)!
|
||||||
return ev.startDate < de && ev.endDate > ds
|
if ev.startDate < de && ev.endDate > ds {
|
||||||
|
if start == nil { start = i }
|
||||||
|
end = i
|
||||||
}
|
}
|
||||||
VStack(spacing: 1) {
|
}
|
||||||
ForEach(dayEvs.prefix(2)) { ev in
|
if let s = start { spans.append((ev, s, end)) }
|
||||||
Button { onEventTap(ev) } label: {
|
}
|
||||||
EventLabel(event: ev)
|
// Longer / earlier bars first, then greedily pack into lanes.
|
||||||
|
spans.sort { a, b in a.start != b.start ? a.start < b.start : (a.end - a.start) > (b.end - b.start) }
|
||||||
|
var laneEnd: [Int] = [] // last occupied column index per lane
|
||||||
|
var out: [(ev: CalEvent, start: Int, end: Int, lane: Int)] = []
|
||||||
|
for s in spans {
|
||||||
|
var lane = 0
|
||||||
|
while lane < laneEnd.count && laneEnd[lane] >= s.start { lane += 1 }
|
||||||
|
if lane == laneEnd.count { laneEnd.append(s.end) } else { laneEnd[lane] = s.end }
|
||||||
|
out.append((s.ev, s.start, s.end, lane))
|
||||||
|
}
|
||||||
|
return out
|
||||||
|
}
|
||||||
|
|
||||||
|
private var allDayRow: some View {
|
||||||
|
let bars = allDayBars()
|
||||||
|
let laneCount = max(1, (bars.map { $0.lane }.max() ?? -1) + 1)
|
||||||
|
return GeometryReader { geo in
|
||||||
|
let colW = (geo.size.width - timeColumnWidth) / 7
|
||||||
|
ZStack(alignment: .topLeading) {
|
||||||
|
ForEach(bars, id: \.ev.id) { bar in
|
||||||
|
Button { onEventTap(bar.ev) } label: {
|
||||||
|
EventLabel(event: bar.ev)
|
||||||
.font(.system(size: 9, weight: .medium))
|
.font(.system(size: 9, weight: .medium))
|
||||||
.foregroundStyle(.white)
|
.foregroundStyle(.white)
|
||||||
.lineLimit(1)
|
.lineLimit(1)
|
||||||
.frame(maxWidth: .infinity)
|
.frame(maxWidth: .infinity, alignment: .leading)
|
||||||
.padding(.vertical, 2)
|
.padding(.horizontal, 3)
|
||||||
.background(Color(hex: ev.effectiveColor))
|
.frame(height: allDayLaneHeight - 2)
|
||||||
|
.background(Color(hex: bar.ev.effectiveColor))
|
||||||
.clipShape(RoundedRectangle(cornerRadius: 2))
|
.clipShape(RoundedRectangle(cornerRadius: 2))
|
||||||
}
|
}
|
||||||
.buttonStyle(.plain)
|
.buttonStyle(.plain)
|
||||||
}
|
.frame(width: colW * CGFloat(bar.end - bar.start + 1) - 3)
|
||||||
}
|
.offset(x: timeColumnWidth + colW * CGFloat(bar.start) + 1.5,
|
||||||
.padding(.horizontal, 1)
|
y: CGFloat(bar.lane) * allDayLaneHeight)
|
||||||
.frame(maxWidth: .infinity)
|
|
||||||
.overlay(alignment: .trailing) {
|
|
||||||
Rectangle().fill(Color(hex: lineHex).opacity(gridLineOpacity(lineContrast))).frame(width: 0.5)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
.frame(height: CGFloat(laneCount) * allDayLaneHeight)
|
||||||
.padding(.vertical, 4)
|
.padding(.vertical, 4)
|
||||||
.overlay(alignment: .bottom) { Divider() }
|
.overlay(alignment: .bottom) { Divider() }
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user