| 1 | import SwiftUI |
| 2 | |
| 3 | struct SheetCell: View { |
| 4 | @Bindable var model: SheetModel |
| 5 | let point: Coordinate |
| 6 | @Binding var editing: Coordinate? |
| 7 | var select: () -> Void |
| 8 | var beginEditing: () -> Void = {} |
| 9 | var endEditing: () -> Void |
| 10 | var columnWidth: Double? = nil |
| 11 | var rowHeight: Double? = nil |
| 12 | @Environment(\.colorScheme) private var scheme |
| 13 | private func width(_ col: Int) -> Double { columnWidth ?? model.sizes.cols?[String(col)] ?? 110 } |
| 14 | private func height(_ row: Int) -> Double { rowHeight ?? model.sizes.rows?[String(row)] ?? 32 } |
| 15 | var body: some View { |
| 16 | let cell = model.cells[point] |
| 17 | let style = model.styles[cell?.f ?? ""] |
| 18 | let alignment: Alignment = |
| 19 | style?.a == "center" |
| 20 | ? .center : style?.a == "right" || (style?.a == nil && cell?.n == true) ? .trailing : .leading |
| 21 | return Group { |
| 22 | #if os(macOS) |
| 23 | if editing == point { |
| 24 | SheetCellEditor(text: $model.draft, enabled: model.mayWrite && !model.busy, |
| 25 | label: String(format: sheetsMacString("Edit %@"), point.ref), |
| 26 | commit: { commit(right: $0) }, cancel: { |
| 27 | guard !model.busy else { return } |
| 28 | model.discardAll(); endEditing() |
| 29 | }) |
| 30 | .padding(.horizontal, 5) |
| 31 | .frame(width: width(point.c), height: height(point.r)) |
| 32 | .background(fill(style)) |
| 33 | .overlay { Rectangle().strokeBorder(Color.accentColor, lineWidth: 2) } |
| 34 | .accessibilityLabel(String(format: sheetsMacString("Edit %@"), point.ref)) |
| 35 | } else { display(cell, style: style, alignment: alignment) } |
| 36 | #else |
| 37 | display(cell, style: style, alignment: alignment) |
| 38 | #endif |
| 39 | } |
| 40 | } |
| 41 | private func commit(right: Bool) { |
| 42 | guard !model.busy, model.selection.start == point else { return } |
| 43 | Task { |
| 44 | await model.commit(moveRight: right) |
| 45 | if !model.hasUnconfirmedChanges { endEditing() } |
| 46 | } |
| 47 | } |
| 48 | private func display(_ cell: Cell?, style: CellStyle?, alignment: Alignment) -> some View { |
| 49 | Button(action: select) { |
| 50 | Text(cell?.display ?? "").font(.system(size: 13, weight: style?.b == true ? .bold : .regular)) |
| 51 | .italic(style?.i == true).lineLimit(1).padding(.horizontal, 5) |
| 52 | .frame(width: width(point.c), height: height(point.r), alignment: alignment) |
| 53 | .foregroundStyle(ink(style)).background(fill(style)) |
| 54 | .contentShape(.rect) |
| 55 | .overlay { |
| 56 | Rectangle().strokeBorder( |
| 57 | model.selection.contains(point) ? Color.accentColor : Color.secondary.opacity(0.2), |
| 58 | lineWidth: model.selection.contains(point) ? 2 : 0.5) |
| 59 | } |
| 60 | }.buttonStyle(.plain) |
| 61 | .simultaneousGesture(TapGesture(count: 2).onEnded { if model.mayWrite, !model.busy, !model.hasUnconfirmedChanges { model.select(point); beginEditing() } }) |
| 62 | .accessibilityLabel("\(point.ref), \(cell?.display ?? "Empty")") |
| 63 | .accessibilityValue(style?.lc == true ? "Imported colours have low contrast" : "") |
| 64 | .accessibilityAddTraits(model.selection.contains(point) ? .isSelected : []) |
| 65 | } |
| 66 | private func fill(_ style: CellStyle?) -> Color { |
| 67 | if let hex = style?.fx { return Color(hex: hex) } |
| 68 | if let h = style?.fh, let c = style?.fc { return .oklch(scheme == .dark ? 0.30 : 0.92, c, h) } |
| 69 | return Color.clear |
| 70 | } |
| 71 | private func ink(_ style: CellStyle?) -> Color { |
| 72 | if let hex = style?.k { return Color(hex: hex) } |
| 73 | if let h = style?.kh, let c = style?.kc { return .oklch(scheme == .dark ? 0.80 : 0.42, c, h) } |
| 74 | return .primary |
| 75 | } |
| 76 | } |
| 77 | |