@@ -9,20 +9,52 @@ import AppKit
99import CodeEditTextView
1010
1111extension FoldingRibbonView {
12- /// The context in which the fold is being drawn, including the depth and fold range.
13- struct FoldMarkerDrawingContext {
14- let range : ClosedRange < Int >
15- let depth : UInt
16-
17- /// Increment the depth
18- func incrementDepth( ) -> FoldMarkerDrawingContext {
19- FoldMarkerDrawingContext (
20- range: range,
21- depth: depth + 1
12+ struct FoldCapInfo {
13+ let startIndices : Set < Int >
14+ let endIndices : Set < Int >
15+
16+ init ( _ folds: [ DrawingFoldInfo ] ) {
17+ self . startIndices = folds. reduce ( into: Set < Int > ( ) , { $0. insert ( $1. startLine. index) } )
18+ self . endIndices = folds. reduce ( into: Set < Int > ( ) , { $0. insert ( $1. endLine. index) } )
19+ }
20+
21+ func foldNeedsTopCap( _ fold: DrawingFoldInfo ) -> Bool {
22+ endIndices. contains ( fold. startLine. index)
23+ }
24+
25+ func foldNeedsBottomCap( _ fold: DrawingFoldInfo ) -> Bool {
26+ startIndices. contains ( fold. endLine. index)
27+ }
28+
29+ func adjustFoldRect(
30+ using fold: DrawingFoldInfo ,
31+ rect: NSRect
32+ ) -> NSRect {
33+ let capTop = foldNeedsTopCap ( fold)
34+ let capBottom = foldNeedsBottomCap ( fold)
35+ let yDelta = capTop ? fold. startLine. height / 2.0 : 0.0
36+ let heightDelta : CGFloat = if capTop && capBottom {
37+ - fold. startLine. height
38+ } else if capTop || capBottom {
39+ - ( fold. startLine. height / 2.0 )
40+ } else {
41+ 0.0
42+ }
43+ return NSRect (
44+ x: rect. origin. x,
45+ y: rect. origin. y + yDelta,
46+ width: rect. size. width,
47+ height: rect. size. height + heightDelta
2248 )
2349 }
2450 }
2551
52+ struct DrawingFoldInfo {
53+ let fold : FoldRange
54+ let startLine : TextLineStorage < TextLine > . TextLinePosition
55+ let endLine : TextLineStorage < TextLine > . TextLinePosition
56+ }
57+
2658 override func draw( _ dirtyRect: NSRect ) {
2759 guard let context = NSGraphicsContext . current? . cgContext,
2860 let layoutManager = model? . controller? . textView. layoutManager else {
@@ -38,19 +70,21 @@ extension FoldingRibbonView {
3870 return
3971 }
4072 let textRange = rangeStart. range. location..< rangeEnd. range. upperBound
41-
42- let folds = getDrawingFolds ( forTextRange : textRange )
43- for fold in folds. filter ( { !$0. isCollapsed } ) {
73+ let folds = getDrawingFolds ( forTextRange : textRange , layoutManager : layoutManager )
74+ let foldCaps = FoldCapInfo ( folds )
75+ for fold in folds. filter ( { !$0. fold . isCollapsed } ) {
4476 drawFoldMarker (
4577 fold,
78+ foldCaps: foldCaps,
4679 in: context,
4780 using: layoutManager
4881 )
4982 }
5083
51- for fold in folds. filter ( { $0. isCollapsed } ) {
84+ for fold in folds. filter ( { $0. fold . isCollapsed } ) {
5285 drawFoldMarker (
5386 fold,
87+ foldCaps: foldCaps,
5488 in: context,
5589 using: layoutManager
5690 )
@@ -59,7 +93,10 @@ extension FoldingRibbonView {
5993 context. restoreGState ( )
6094 }
6195
62- private func getDrawingFolds( forTextRange textRange: Range < Int > ) -> [ FoldRange ] {
96+ private func getDrawingFolds(
97+ forTextRange textRange: Range < Int > ,
98+ layoutManager: TextLayoutManager
99+ ) -> [ DrawingFoldInfo ] {
63100 var folds = model? . getFolds ( in: textRange) ?? [ ]
64101
65102 // Add in some fake depths, we can draw these underneath the rest of the folds to make it look like it's
@@ -78,44 +115,47 @@ extension FoldingRibbonView {
78115 }
79116 }
80117
81- return folds
118+ return folds. compactMap { fold in
119+ guard let startLine = layoutManager. textLineForOffset ( fold. range. lowerBound) ,
120+ let endLine = layoutManager. textLineForOffset ( fold. range. upperBound) else {
121+ return nil
122+ }
123+
124+ return DrawingFoldInfo ( fold: fold, startLine: startLine, endLine: endLine)
125+ }
82126 }
83127
84128 /// Draw a single fold marker for a fold.
85129 ///
86130 /// Ensure the correct fill color is set on the drawing context before calling.
87131 ///
88132 /// - Parameters:
89- /// - fold : The fold to draw.
133+ /// - foldInfo : The fold to draw.
90134 /// - markerContext: The context in which the fold is being drawn, including the depth and if a line is
91135 /// being hovered.
92136 /// - context: The drawing context to use.
93137 /// - layoutManager: A layout manager used to retrieve position information for lines.
94138 private func drawFoldMarker(
95- _ fold: FoldRange ,
139+ _ foldInfo: DrawingFoldInfo ,
140+ foldCaps: FoldCapInfo ,
96141 in context: CGContext ,
97142 using layoutManager: TextLayoutManager
98143 ) {
99- guard let minYPosition = layoutManager. textLineForOffset ( fold. range. lowerBound) ? . yPos,
100- let maxPosition = layoutManager. textLineForOffset ( fold. range. upperBound) else {
101- return
102- }
103-
104- let maxYPosition = maxPosition. yPos + maxPosition. height
144+ let minYPosition = foldInfo. startLine. yPos
145+ let maxYPosition = foldInfo. endLine. yPos + foldInfo. endLine. height
105146
106- if fold. isCollapsed {
147+ if foldInfo . fold. isCollapsed {
107148 drawCollapsedFold ( minYPosition: minYPosition, maxYPosition: maxYPosition, in: context)
108- } else if let hoveringFold,
109- hoveringFold. depth == fold. depth,
110- NSRange ( hoveringFold. range) . intersection ( NSRange ( fold. range) ) == NSRange ( hoveringFold. range) {
149+ } else if let hoveringFold, hoveringFold. isHoveringEqual ( foldInfo. fold) {
111150 drawHoveredFold (
112151 minYPosition: minYPosition,
113152 maxYPosition: maxYPosition,
114153 in: context
115154 )
116155 } else {
117156 drawNestedFold (
118- fold: fold,
157+ foldInfo: foldInfo,
158+ foldCaps: foldCaps,
119159 minYPosition: minYPosition,
120160 maxYPosition: maxYPosition,
121161 in: context
@@ -204,26 +244,37 @@ extension FoldingRibbonView {
204244 }
205245
206246 private func drawNestedFold(
207- fold: FoldRange ,
247+ foldInfo: DrawingFoldInfo ,
248+ foldCaps: FoldCapInfo ,
208249 minYPosition: CGFloat ,
209250 maxYPosition: CGFloat ,
210251 in context: CGContext
211252 ) {
212253 context. saveGState ( )
213- let plainRect = NSRect ( x: 0 , y: minYPosition + 1 , width: 7 , height: maxYPosition - minYPosition - 2 )
214- // TODO: Draw a single horizontal line when folds are adjacent
215- let roundedRect = NSBezierPath ( roundedRect: plainRect, xRadius: 3.5 , yRadius: 3.5 )
254+ let plainRect = foldCaps. adjustFoldRect (
255+ using: foldInfo,
256+ rect: NSRect ( x: 0 , y: minYPosition + 1 , width: 7 , height: maxYPosition - minYPosition - 2 )
257+ )
258+ let radius = plainRect. width / 2.0
259+ let roundedRect = NSBezierPath (
260+ roundingRect: plainRect,
261+ capTop: foldCaps. foldNeedsTopCap ( foldInfo) ,
262+ capBottom: foldCaps. foldNeedsBottomCap ( foldInfo) ,
263+ cornerRadius: radius
264+ )
216265
217266 context. setFillColor ( markerColor)
218267 context. addPath ( roundedRect. cgPathFallback)
219268 context. drawPath ( using: . fill)
220269
221270 // Add small white line if we're overlapping with other markers
222- if fold. depth != 0 {
271+ if foldInfo . fold. depth != 0 {
223272 drawOutline (
273+ foldInfo: foldInfo,
274+ foldCaps: foldCaps,
275+ originalPath: roundedRect. cgPathFallback,
224276 minYPosition: minYPosition,
225277 maxYPosition: maxYPosition,
226- originalPath: roundedRect,
227278 in: context
228279 )
229280 }
@@ -241,19 +292,31 @@ extension FoldingRibbonView {
241292 /// - originalPath: The original bezier path for the rounded rectangle.
242293 /// - context: The context to draw in.
243294 private func drawOutline(
295+ foldInfo: DrawingFoldInfo ,
296+ foldCaps: FoldCapInfo ,
297+ originalPath: CGPath ,
244298 minYPosition: CGFloat ,
245299 maxYPosition: CGFloat ,
246- originalPath: NSBezierPath ,
247300 in context: CGContext
248301 ) {
249302 context. saveGState ( )
250303
251- let plainRect = NSRect ( x: - 0.5 , y: minYPosition, width: 8 , height: maxYPosition - minYPosition)
252- let roundedRect = NSBezierPath ( roundedRect: plainRect, xRadius: 4 , yRadius: 4 )
304+ let plainRect = foldCaps. adjustFoldRect (
305+ using: foldInfo,
306+ rect: NSRect ( x: - 0.5 , y: minYPosition, width: frame. width + 1.0 , height: maxYPosition - minYPosition)
307+ )
308+ let radius = plainRect. width / 2.0
309+ let roundedRect = NSBezierPath (
310+ roundingRect: plainRect,
311+ capTop: foldCaps. foldNeedsTopCap ( foldInfo) ,
312+ capBottom: foldCaps. foldNeedsBottomCap ( foldInfo) ,
313+ cornerRadius: radius
314+ )
315+ roundedRect. transform ( using: . init( translationByX: - 0.5 , byY: 0.0 ) )
253316
254317 let combined = CGMutablePath ( )
255318 combined. addPath ( roundedRect. cgPathFallback)
256- combined. addPath ( originalPath. cgPathFallback )
319+ combined. addPath ( originalPath)
257320
258321 context. clip ( to: CGRect ( x: 0 , y: minYPosition, width: 7 , height: maxYPosition - minYPosition) )
259322 context. addPath ( combined)
0 commit comments