|
| 1 | +import Cocoa |
| 2 | + |
| 3 | +final class Accessory { |
| 4 | + var isSideAccessory = true |
| 5 | + var size = NSSize.zero |
| 6 | + var width: CGFloat = 0 |
| 7 | + var longQuote = false |
| 8 | + func measureSize(_ width: CGFloat, sizeToFit: Bool) { |
| 9 | + self.width = width |
| 10 | + let lines = longQuote ? min(8, ceil(700 / max(1, width - 14))) : 1 |
| 11 | + size = NSSize(width: sizeToFit ? min(180, max(1, width)) + 12 : width, height: 22 + 16 * lines) |
| 12 | + } |
| 13 | +} |
| 14 | +struct Mode { var isThreadMode = false } |
| 15 | +struct Location { var threadMsgId: Int? } |
| 16 | +struct Interaction { let mode = Mode(); let chatLocation = Location() } |
| 17 | +struct Message { let id: Int } |
| 18 | +final class ChatRowItem { |
| 19 | + var width: CGFloat = 320 |
| 20 | + var leftInset: CGFloat = 13 |
| 21 | + var rightInset: CGFloat = 12 |
| 22 | + var defLeftInset: CGFloat = 20 |
| 23 | + var isIncoming = false |
| 24 | + var isBubbled = true |
| 25 | + var hasBubble = false |
| 26 | + var isBubbleFullFilled = true |
| 27 | + var isSharable = false |
| 28 | + var hasSource = false |
| 29 | + var commentsBubbleDataOverlay: Int? |
| 30 | + var elementsContentInset: CGFloat = 0 |
| 31 | + var bubbleContentInset: CGFloat = 0 |
| 32 | + var additionBubbleInset: CGFloat = 6 |
| 33 | + var bubbleDefaultInnerInset: CGFloat { bubbleContentInset * 2 + additionBubbleInset } |
| 34 | + var _contentSize = NSSize(width: 160, height: 80) |
| 35 | + var contentSize: NSSize { _contentSize } |
| 36 | + var contentOffset: NSPoint { NSPoint(x: defLeftInset, y: 2) } |
| 37 | + var bubbleFrameWidth: CGFloat { _contentSize.width + additionBubbleInset } |
| 38 | + var max_reply_size_width: CGFloat { _contentSize.width } |
| 39 | + var additionalLineForDateInBubbleState: CGFloat? |
| 40 | + var defaultContentTopOffset: CGFloat = 2 |
| 41 | + var replyOffset: CGFloat = 5 |
| 42 | + var replyModel: Accessory? = Accessory() |
| 43 | + let chatInteraction = Interaction() |
| 44 | + var message: Message? |
| 45 | + func measureReply() { |
| 46 | + let cachedBlockWidth = width |
| 47 | + if let replyModel = replyModel { __MEASURE__ } |
| 48 | + } |
| 49 | + var height: CGFloat { |
| 50 | + var height = _contentSize.height + defaultContentTopOffset * 2 |
| 51 | + __HEIGHT__ |
| 52 | + return height |
| 53 | + } |
| 54 | + var replyFrame: NSRect { |
| 55 | + let bubble = ChatRowGeometry.bubbleFrame(.init(rowWidth: width, layoutFrame: NSRect(x: defLeftInset, y: 0, width: bubbleFrameWidth, height: 0), leftInset: leftInset, isIncoming: isIncoming, isCenteredThread: false)) |
| 56 | + let content = ChatRowGeometry.contentFrame(.init(item: self, bubbleFrame: bubble)) |
| 57 | + return ChatRowGeometry.sideAccessoryFrame(.init(item: self, size: replyModel!.size, offset: replyOffset, contentFrame: content)) |
| 58 | + } |
| 59 | +} |
| 60 | +var checks = 0 |
| 61 | +var failures = 0 |
| 62 | +func expect(_ condition: @autoclosure () -> Bool, _ name: String) { |
| 63 | + checks += 1 |
| 64 | + if !condition() { failures += 1; print("FAIL: \(name)") } |
| 65 | +} |
| 66 | +for width in [CGFloat(300), 320, 350, 400, 640, 1280] { |
| 67 | + for incoming in [false, true] { |
| 68 | + for sideAction in [false, true] { |
| 69 | + for contentWidth in [CGFloat(80), 160, 200] { |
| 70 | + for longQuote in [false, true] { |
| 71 | + let row = ChatRowItem() |
| 72 | + row.width = width |
| 73 | + row.isIncoming = incoming |
| 74 | + row.isSharable = sideAction |
| 75 | + row._contentSize.width = contentWidth |
| 76 | + row.replyModel!.longQuote = longQuote |
| 77 | + row.measureReply() |
| 78 | + let frame = row.replyFrame |
| 79 | + let tag = "width=\(width) incoming=\(incoming) action=\(sideAction) content=\(contentWidth) quote=\(longQuote)" |
| 80 | + expect(frame.minX >= row.leftInset, "reply keeps left margin: \(tag)") |
| 81 | + expect(frame.maxX <= width - row.rightInset, "reply keeps right margin: \(tag)") |
| 82 | + expect(frame.maxY <= row.height - row.defaultContentTopOffset, "entire quoted reply fits row height: \(tag)") |
| 83 | + } |
| 84 | + } |
| 85 | + } |
| 86 | + } |
| 87 | +} |
| 88 | +let row = ChatRowItem() |
| 89 | +row.width = 1280 |
| 90 | +row.measureReply() |
| 91 | +expect(row.replyModel!.size.width == 172, "wide layout keeps intrinsic side reply sizing") |
| 92 | +row.width = 320 |
| 93 | +row.measureReply() |
| 94 | +expect(row.replyFrame.minX >= row.leftInset, "resizing narrows existing reply") |
| 95 | +row.width = 1280 |
| 96 | +row.measureReply() |
| 97 | +expect(row.replyModel!.size.width == 172, "resizing restores intrinsic side reply width") |
| 98 | +print("\(checks - failures)/\(checks) side reply layout checks passed") |
| 99 | +exit(failures == 0 ? 0 : 1) |
0 commit comments