Skip to content

Commit ca551f0

Browse files
authored
Background border overlap fix (#196)
* Fixed overlapping of background style * Removed alpha from bg color in tests owing to failures on CI * Fixed background in editor with textContainerInsets defined * Introduced option to draw tighter background around text
1 parent 54f28d1 commit ca551f0

10 files changed

+185
-12
lines changed

Diff for: Proton/Sources/Swift/Base/BackgroundStyle.swift

+12-1
Original file line numberDiff line numberDiff line change
@@ -62,12 +62,23 @@ public enum RoundedCornerStyle {
6262
/// Rounding based on relative percent value of the content height. For e.g. 50% would provide a capsule appearance
6363
/// for shorter content.
6464
case relative(percent: CGFloat)
65+
66+
public var isRelative: Bool {
67+
switch self {
68+
case .absolute:
69+
return false
70+
case .relative:
71+
return true
72+
}
73+
}
6574
}
6675

6776
/// Defines the mode for height/width used for the background for the text
6877
public enum BackgroundMode {
69-
/// Background matches the height/width of text
78+
/// Background matches the height/width of text with font leading padding all around
7079
case matchText
80+
/// Background matches the height of text with minimal padding all around
81+
case matchTextExact
7182
/// Background matches entire line irrespective of font height/used character width in the given line
7283
case matchLine
7384
}

Diff for: Proton/Sources/Swift/Core/LayoutManager.swift

+40-11
Original file line numberDiff line numberDiff line change
@@ -278,17 +278,19 @@ class LayoutManager: NSLayoutManager {
278278
let rangeIntersection = NSIntersectionRange(bgStyleGlyphRange, lineRange)
279279
var rect = self.boundingRect(forGlyphRange: rangeIntersection, in: textContainer)
280280

281-
var contentSize: CGSize?
282281
if backgroundStyle.widthMode == .matchText {
283282
let content = textStorage.attributedSubstring(from: rangeIntersection)
284283
let contentWidth = content.boundingRect(with: rect.size, options: [.usesDeviceMetrics, .usesFontLeading], context: nil).width
285284
rect.size.width = contentWidth
286285
}
287286

288287
switch backgroundStyle.heightMode {
289-
case .matchText:
288+
case .matchText,
289+
.matchTextExact:
290290
let styledText = textStorage.attributedSubstring(from: bgStyleGlyphRange)
291-
let textRect = styledText.boundingRect(with: rect.size, options: [.usesLineFragmentOrigin, .usesFontLeading, .usesDeviceMetrics], context: nil)
291+
let drawingOptions = backgroundStyle.heightMode == .matchText ? NSStringDrawingOptions.usesFontLeading : []
292+
293+
let textRect = styledText.boundingRect(with: rect.size, options: drawingOptions, context: nil)
292294

293295
rect.origin.y = usedRect.origin.y + (rect.size.height - textRect.height)
294296
rect.size.height = textRect.height
@@ -300,8 +302,8 @@ class LayoutManager: NSLayoutManager {
300302

301303
}
302304

303-
let insetTop = self.layoutManagerDelegate?.textContainerInset.top ?? 0
304-
rects.append(rect.offsetBy(dx: 0, dy: insetTop))
305+
let inset = self.layoutManagerDelegate?.textContainerInset ?? .zero
306+
rects.append(rect.offsetBy(dx: inset.left, dy: inset.top))
305307
}
306308
drawBackground(backgroundStyle: backgroundStyle, rects: rects, currentCGContext: currentCGContext)
307309
}
@@ -374,11 +376,26 @@ class LayoutManager: NSLayoutManager {
374376
// Shadow for vertical lines need to be drawn separately to get the perfect alignment with shadow on rectangles.
375377
let leftVerticalJoiningLineShadow = UIBezierPath()
376378
let rightVerticalJoiningLineShadow = UIBezierPath()
379+
var lineLength: CGFloat = 0
377380

378-
if !previousRect.isEmpty, (currentRect.maxX - previousRect.minX) > cornerRadius {
381+
if backgroundStyle.heightMode != .matchTextExact,
382+
!previousRect.isEmpty, (currentRect.maxX - previousRect.minX) > cornerRadius {
379383
let yDiff = currentRect.minY - previousRect.maxY
380-
overlappingLine.move(to: CGPoint(x: max(previousRect.minX, currentRect.minX) + lineWidth/2, y: previousRect.maxY + yDiff/2))
381-
overlappingLine.addLine(to: CGPoint(x: min(previousRect.maxX, currentRect.maxX) - lineWidth/2, y: previousRect.maxY + yDiff/2))
384+
var overLapMinX = max(previousRect.minX, currentRect.minX) + lineWidth/2
385+
var overlapMaxX = min(previousRect.maxX, currentRect.maxX) - lineWidth/2
386+
lineLength = overlapMaxX - overLapMinX
387+
388+
// Adjust overlap line length if the rounding on current and previous overlaps
389+
// accounting for relative rounding as it rounds at both top and bottom vs. fixed which rounds
390+
// only at top when in an overlap
391+
if (currentRect.maxX - previousRect.minX <= cornerRadius)
392+
|| (previousRect.minX - currentRect.maxX <= cornerRadius) && backgroundStyle.roundedCornerStyle.isRelative {
393+
overLapMinX += cornerRadius
394+
overlapMaxX -= cornerRadius
395+
}
396+
397+
overlappingLine.move(to: CGPoint(x: overLapMinX , y: previousRect.maxY + yDiff/2))
398+
overlappingLine.addLine(to: CGPoint(x: overlapMaxX, y: previousRect.maxY + yDiff/2))
382399

383400
let leftX = max(previousRect.minX, currentRect.minX)
384401
let rightX = min(previousRect.maxX, currentRect.maxX)
@@ -425,9 +442,15 @@ class LayoutManager: NSLayoutManager {
425442
currentCGContext.drawPath(using: .stroke)
426443
}
427444

428-
// always draw over the overlapping bounds of previous and next rect to hide shadow/borders
429-
currentCGContext.setStrokeColor(color.cgColor)
430-
currentCGContext.addPath(overlappingLine.cgPath)
445+
// draw over the overlapping bounds of previous and next rect to hide shadow/borders
446+
// if the border color is defined and different from background
447+
// Also, account for rounding so that the overlap line does not eat into rounding lines
448+
if let borderColor = backgroundStyle.border?.color,
449+
lineLength > (cornerRadius * 2),
450+
color != borderColor {
451+
currentCGContext.setStrokeColor(color.cgColor)
452+
currentCGContext.addPath(overlappingLine.cgPath)
453+
}
431454
// account for the spread of shadow
432455
let blur = (backgroundStyle.shadow?.blur ?? 1) * 2
433456
let offsetHeight = abs(backgroundStyle.shadow?.offset.height ?? 1)
@@ -529,3 +552,9 @@ extension UIImage {
529552
return scaledImage
530553
}
531554
}
555+
556+
extension CGFloat {
557+
func isBetween(_ first: CGFloat, _ second: CGFloat) -> Bool {
558+
return self > first && self < second
559+
}
560+
}

Diff for: Proton/Tests/Editor/EditorSnapshotTests.swift

+133
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,11 @@ import SnapshotTesting
2525
@testable import Proton
2626

2727
class EditorSnapshotTests: SnapshotTestCase {
28+
override func setUp() {
29+
super.setUp()
30+
recordMode = false
31+
}
32+
2833
func testRendersPlaceholder() {
2934
let viewController = EditorTestViewController(height: 80)
3035
let editor = viewController.editor
@@ -783,6 +788,134 @@ class EditorSnapshotTests: SnapshotTestCase {
783788
assertSnapshot(matching: viewController.view, as: .image, record: recordMode)
784789
}
785790

791+
func testBackgroundStyleWithHeightMatchingText() {
792+
let viewController = EditorTestViewController()
793+
let editor = viewController.editor
794+
795+
let text =
796+
"""
797+
Line 1 Text\nLine 2 Text
798+
"""
799+
800+
let rangeToUpdate = NSRange(location: 5, length: 14)
801+
802+
editor.appendCharacters(NSAttributedString(string: text))
803+
viewController.render()
804+
let backgroundStyle = BackgroundStyle(color: .cyan.withAlphaComponent(0.5),
805+
roundedCornerStyle: .relative(percent: 50),
806+
border: BorderStyle(lineWidth: 1, color: .yellow),
807+
hasSquaredOffJoins: true,
808+
heightMode: .matchText)
809+
editor.addAttributes([
810+
.backgroundStyle: backgroundStyle
811+
], at: rangeToUpdate)
812+
813+
viewController.render(size: CGSize(width: 130, height: 100))
814+
assertSnapshot(matching: viewController.view, as: .image, record: recordMode)
815+
}
816+
817+
func testBackgroundStyleWithTextContainerInsets() {
818+
let viewController = EditorTestViewController()
819+
let editor = viewController.editor
820+
editor.textContainerInset = UIEdgeInsets(top: 10, left: 10, bottom: 10, right: 10)
821+
let text =
822+
"""
823+
Line 1 Text\nLine 2 Text
824+
"""
825+
826+
let rangeToUpdate = NSRange(location: 5, length: 14)
827+
828+
editor.appendCharacters(NSAttributedString(string: text))
829+
viewController.render()
830+
let backgroundStyle = BackgroundStyle(color: .cyan,
831+
roundedCornerStyle: .relative(percent: 50),
832+
hasSquaredOffJoins: true,
833+
heightMode: .matchText)
834+
editor.addAttributes([
835+
.backgroundStyle: backgroundStyle
836+
], at: rangeToUpdate)
837+
838+
viewController.render(size: CGSize(width: 160, height: 100))
839+
assertSnapshot(matching: viewController.view, as: .image, record: recordMode)
840+
}
841+
842+
func testBackgroundStyleWithOverlappingLineNoBorder() {
843+
let viewController = EditorTestViewController()
844+
let editor = viewController.editor
845+
846+
let text =
847+
"""
848+
Line 1 Text\nLine 2 Text
849+
"""
850+
851+
let rangeToUpdate = NSRange(location: 5, length: 14)
852+
853+
editor.appendCharacters(NSAttributedString(string: text))
854+
viewController.render()
855+
let backgroundStyle = BackgroundStyle(color: .cyan,
856+
roundedCornerStyle: .relative(percent: 50),
857+
hasSquaredOffJoins: true,
858+
heightMode: .matchText)
859+
editor.addAttributes([
860+
.backgroundStyle: backgroundStyle
861+
], at: rangeToUpdate)
862+
863+
viewController.render(size: CGSize(width: 130, height: 100))
864+
assertSnapshot(matching: viewController.view, as: .image, record: recordMode)
865+
}
866+
867+
func testBackgroundStyleWithOverlappingLine() {
868+
let viewController = EditorTestViewController()
869+
let editor = viewController.editor
870+
871+
let text =
872+
"""
873+
Line 1 Text\nLine 2 Text
874+
"""
875+
876+
let rangeToUpdate = NSRange(location: 5, length: 16)
877+
878+
editor.appendCharacters(NSAttributedString(string: text))
879+
viewController.render()
880+
let backgroundStyle = BackgroundStyle(color: .cyan,
881+
roundedCornerStyle: .relative(percent: 50),
882+
border: BorderStyle(lineWidth: 1, color: .black),
883+
hasSquaredOffJoins: true,
884+
heightMode: .matchText)
885+
editor.addAttributes([
886+
.backgroundStyle: backgroundStyle
887+
], at: rangeToUpdate)
888+
889+
viewController.render(size: CGSize(width: 150, height: 100))
890+
assertSnapshot(matching: viewController.view, as: .image, record: recordMode)
891+
}
892+
893+
func testBackgroundStyleWithOverlappingLineExactTextHeight() {
894+
let viewController = EditorTestViewController()
895+
let editor = viewController.editor
896+
897+
let text =
898+
"""
899+
Line 1 Text\nLine 2 Text
900+
"""
901+
902+
let rangeToUpdate = NSRange(location: 5, length: 16)
903+
904+
editor.appendCharacters(NSAttributedString(string: text))
905+
viewController.render()
906+
let backgroundStyle = BackgroundStyle(color: .cyan,
907+
roundedCornerStyle: .relative(percent: 50),
908+
// border: BorderStyle(lineWidth: 1, color: .black),
909+
hasSquaredOffJoins: true,
910+
heightMode: .matchTextExact)
911+
editor.addAttributes([
912+
.backgroundStyle: backgroundStyle
913+
], at: rangeToUpdate)
914+
915+
viewController.render(size: CGSize(width: 150, height: 100))
916+
assertSnapshot(matching: viewController.view, as: .image, record: recordMode)
917+
}
918+
786919
func testBackgroundStyleWithVariedFontSizes() {
787920
let viewController = EditorTestViewController()
788921
let editor = viewController.editor
Loading
Loading
Loading
Loading
Loading
Loading

0 commit comments

Comments
 (0)