@@ -2,71 +2,265 @@ Class {
2
2
#name : #BrTextEditorParagraphElement ,
3
3
#superclass : #BlElement ,
4
4
#instVars : [
5
- ' paragraph'
5
+ ' paragraph' ,
6
+ ' text' ,
7
+ ' cursorElements' ,
8
+ ' cursorStencil' ,
9
+ ' selection'
10
+ ],
11
+ #classInstVars : [
12
+ ' aCompositorPainter'
6
13
],
7
14
#category : #' Brick-Editor-UI'
8
15
}
9
16
10
- { #category : #' hooks - children' }
17
+ { #category : #' api - cursor' }
18
+ BrTextEditorParagraphElement >> addCursorAt: aTextPosition [
19
+ < return: #BrCursorElement >
20
+
21
+ ^ self
22
+ cursorAt: aTextPosition
23
+ ifFound: #yourself
24
+ ifNone: [
25
+ | aCursorElement |
26
+ aCursorElement := self newCursor.
27
+ aCursorElement textPosition: aTextPosition.
28
+ cursorElements add: aCursorElement.
29
+ self addChild: aCursorElement.
30
+ aCursorElement ]
31
+ ]
32
+
33
+ { #category : #' private - paragraph' }
34
+ BrTextEditorParagraphElement >> addText: aBlText toBuilder: aParagraphBuilder [
35
+ | anIterator |
36
+
37
+ anIterator := aBlText iterator.
38
+ [ anIterator hasNext ] whileTrue: [
39
+ | eachSpan |
40
+
41
+ eachSpan := anIterator nextSpan.
42
+
43
+ eachSpan attributes ifNotEmpty: [ :theAttributes |
44
+ | aStyleBuilder |
45
+
46
+ aStyleBuilder := BrTextEditorParagraphTextStyleBuilder new .
47
+ theAttributes do: [ :eachAttribute | eachAttribute applyOnSpartaFontBuilder: aStyleBuilder ].
48
+ theAttributes do: [ :eachAttribute | eachAttribute applyOnSpartaTextPainter: aStyleBuilder ].
49
+
50
+ SkiaParagraphTextStyle newDuring: [ :aTextStyle |
51
+ aStyleBuilder hasCustomFontStyle
52
+ ifTrue: [ aStyleBuilder asFontStyleDuring: [ :aSkiaFontStyle | aTextStyle fontStyle: aSkiaFontStyle ] ].
53
+
54
+ aStyleBuilder hasCustomFontSize
55
+ ifTrue: [ aTextStyle fontSize: aStyleBuilder fontSize ]
56
+ ifFalse: [ aTextStyle fontSize: self defaultFontSize ].
57
+
58
+ aStyleBuilder hasCustomFamilyName
59
+ ifTrue: [ aTextStyle fontFamily: aStyleBuilder familyName ].
60
+
61
+ aStyleBuilder hasCustomColor
62
+ ifTrue: [ aTextStyle color: aStyleBuilder color ]
63
+ ifFalse: [ aTextStyle color: self defaultTextColor ].
64
+
65
+ aParagraphBuilder
66
+ pushStyle: aTextStyle;
67
+ addString: eachSpan asString;
68
+ popStyle
69
+
70
+ ].
71
+ ]
72
+ ifEmpty: [ aParagraphBuilder addString: eachSpan asString ].
73
+
74
+ ]
75
+ ]
76
+
77
+ { #category : #' private - paragraph' }
11
78
BrTextEditorParagraphElement >> buildParagraph [
12
79
| aBuilder |
80
+
13
81
aBuilder := SkiaParagraphBuilder
14
- style: (SkiaParagraphStyle new textStyle: (SkiaParagraphTextStyle new color: Color black))
82
+ style: (SkiaParagraphStyle new textStyle: (SkiaParagraphTextStyle new color: Color black; fontSize: self defaultFontSize ))
15
83
fontCollection: (SkiaFontCollection new defaultFontManager: SkiaFontManager default).
16
84
17
85
self childrenDo: [ :eachElement |
18
86
(eachElement isKindOf: BlTextElement )
19
- ifTrue: [ aBuilder addString: eachElement text asString ]
20
- ifFalse: [ aBuilder addPlaceholder: (SkiaParagraphPlaceholderStyle new alignTop extent: eachElement measuredExtent) ] ].
21
-
87
+ ifTrue: [ self addText: eachElement text toBuilder: aBuilder ]
88
+ ifFalse: [
89
+ (eachElement isKindOf: BrCursorElement )
90
+ ifFalse: [ aBuilder addPlaceholder: (SkiaParagraphPlaceholderStyle new alignTop extent: eachElement measuredExtent) ] ] ].
91
+
22
92
^ aBuilder build
23
93
]
24
94
95
+ { #category : #private }
96
+ BrTextEditorParagraphElement >> cursorAt: aTextPosition ifFound: aFoundBlock ifNone: aNoneBlock [
97
+
98
+ ^ cursorElements
99
+ detect: [ :aCursor | aCursor textPosition = aTextPosition ]
100
+ ifFound: aFoundBlock
101
+ ifNone: aNoneBlock
102
+ ]
103
+
104
+ { #category : #accessing }
105
+ BrTextEditorParagraphElement >> cursorElements [
106
+ < return: #Collection of: #BrCursorElement >
107
+
108
+ ^ cursorElements
109
+ ]
110
+
111
+ { #category : #accessing }
112
+ BrTextEditorParagraphElement >> cursorStencil [
113
+ ^ cursorStencil
114
+ ]
115
+
116
+ { #category : #accessing }
117
+ BrTextEditorParagraphElement >> cursorStencil: aStencil [
118
+ cursorStencil := aStencil
119
+ ]
120
+
121
+ { #category : #initialization }
122
+ BrTextEditorParagraphElement >> defaultFontSize [
123
+ ^ 12
124
+ ]
125
+
126
+ { #category : #initialization }
127
+ BrTextEditorParagraphElement >> defaultSelectionColor [
128
+ ^ (Color r: 0 g: 112 b: 252 range: 255 ) alpha: 0.3
129
+ ]
130
+
131
+ { #category : #initialization }
132
+ BrTextEditorParagraphElement >> defaultTextColor [
133
+ ^ Color black
134
+ ]
135
+
25
136
{ #category : #drawing }
26
137
BrTextEditorParagraphElement >> drawChildrenOnSpartaCanvas: aCanvas [
27
138
aCanvas clip
28
139
when: [ self clipChildren ]
29
140
by: [ self geometry pathOnSpartaCanvas: aCanvas of: self ]
30
141
during: [
31
- paragraph paintOn: aCanvas at: 0 @ 0 .
142
+ paragraph paintOn: aCanvas at: self padding topLeft .
32
143
self children sortedByElevation
33
- do: [ :anElement |
144
+ do: [ :anElement |
34
145
(anElement isKindOf: BlTextElement )
35
146
ifFalse: [ anElement fullDrawOnSpartaCanvas: aCanvas ] ] ]
36
147
]
37
148
149
+ { #category : #drawing }
150
+ BrTextEditorParagraphElement >> drawSelectionOnSpartaCanvas: aCanvas [
151
+ | aPathBuilder |
152
+
153
+ selection isEmpty
154
+ ifTrue: [ ^ self ].
155
+
156
+ aPathBuilder := aCanvas path.
157
+
158
+ selection do: [ :eachMonotoneSelection |
159
+ (paragraph rectanglesForChars: (eachMonotoneSelection from to: eachMonotoneSelection to)
160
+ width: SkiaParagraphRectWidthStyle Max
161
+ height: SkiaParagraphRectHeightStyle Max ) do: [ :eachRectangle |
162
+ aPathBuilder
163
+ moveTo: eachRectangle topLeft;
164
+ lineTo: eachRectangle topRight;
165
+ lineTo: eachRectangle bottomRight;
166
+ lineTo: eachRectangle bottomLeft;
167
+ lineTo: eachRectangle topLeft;
168
+ close ] ].
169
+
170
+ aCanvas fill
171
+ path: aPathBuilder build;
172
+ paint: self defaultSelectionColor;
173
+ draw
174
+ ]
175
+
176
+ { #category : #' api - cursor' }
177
+ BrTextEditorParagraphElement >> hideCursors [
178
+ self cursorElements do: [ :aCursorElement | aCursorElement visibility: BlVisibility gone ]
179
+ ]
180
+
181
+ { #category : #initialization }
182
+ BrTextEditorParagraphElement >> initialize [
183
+ super initialize.
184
+
185
+ cursorElements := OrderedCollection new .
186
+ cursorStencil := BrCursorStencil uniqueInstance.
187
+ text := BlText empty.
188
+ selection := BlSelection empty
189
+ ]
190
+
191
+ { #category : #layout }
192
+ BrTextEditorParagraphElement >> measureCursors: aCollectionOfCursorElements [
193
+ aCollectionOfCursorElements do: [ :eachCursor |
194
+ | aTextPosition theRectangles aCharInterval |
195
+
196
+ aTextPosition := eachCursor textPosition.
197
+
198
+ aCharInterval := (aTextPosition - 1 max: 0 )
199
+ to: ((aTextPosition max: 1 ) min: self text size).
200
+
201
+ theRectangles := paragraph
202
+ rectanglesForChars: aCharInterval
203
+ width: SkiaParagraphRectWidthStyle Tight
204
+ height: SkiaParagraphRectHeightStyle Max .
205
+
206
+ theRectangles ifNotEmpty: [
207
+ | thePosition |
208
+
209
+ thePosition := aTextPosition isZero
210
+ ifTrue: [ theRectangles first topLeft ]
211
+ ifFalse: [ theRectangles first topRight ].
212
+
213
+ eachCursor measuredBounds
214
+ extent: (eachCursor measuredWidth max: 1 ) @ (theRectangles first height max: 10 );
215
+ position: thePosition + self padding topLeft ] ]
216
+ ]
217
+
218
+ { #category : #private }
219
+ BrTextEditorParagraphElement >> newCursor [
220
+ " Create and return a new instance of a cursor element"
221
+ < return: #BlElement >
222
+
223
+ ^ self cursorStencil asElement
224
+ ]
225
+
38
226
{ #category : #' hooks - children' }
39
227
BrTextEditorParagraphElement >> onChildAdded: anElement [
40
228
super onChildAdded: anElement.
41
229
42
- paragraph := nil
230
+ (anElement isKindOf: BrCursorElement )
231
+ ifFalse: [ paragraph := nil ]
232
+
233
+
43
234
]
44
235
45
- { #category : #' hooks - children ' }
236
+ { #category : #layout }
46
237
BrTextEditorParagraphElement >> onLayout: aBounds [
47
238
| placeholders |
48
239
49
- placeholders := self children reject: [ :each | each isKindOf: BlTextElement ] .
240
+ placeholders := self placeholderElements .
50
241
placeholders with: paragraph placeholderRectangles do: [ :eachElement :eachBounds |
51
- eachElement applyLayoutIn: eachBounds ].
242
+ eachElement applyLayoutIn: (eachBounds translateBy: self padding topLeft) ].
243
+
244
+ self cursorElements do: [ :eachCursor | eachCursor applyLayoutIn: eachCursor measuredBounds asRectangle ].
52
245
]
53
246
54
- { #category : #' hooks - children ' }
247
+ { #category : #layout }
55
248
BrTextEditorParagraphElement >> onMeasure: anExtentMeasurementSpec [
56
- self childrenDo: [ :eachElement |
57
- (eachElement isKindOf: BlTextElement )
58
- ifFalse: [ self layout measureChildWithMargins: eachElement parentSpec: anExtentMeasurementSpec ] ].
249
+ self placeholderAndCursorElements do: [ :eachElement |
250
+ self layout measureChildWithMargins: eachElement parentSpec: anExtentMeasurementSpec ].
59
251
60
252
paragraph := self buildParagraph.
61
253
paragraph layoutWithWidth: anExtentMeasurementSpec widthSpec size.
62
-
63
- self measuredExtent: (anExtentMeasurementSpec sizeFor: paragraph longestLine @ paragraph height)
254
+
255
+ self measuredExtent: (anExtentMeasurementSpec sizeFor: paragraph longestLine @ paragraph height) + self padding extent.
256
+ self cursorElements ifNotEmpty: [ :theCursors | self measureCursors: theCursors ]
64
257
]
65
258
66
259
{ #category : #drawing }
67
260
BrTextEditorParagraphElement >> paintChildrenOn: aCompositorPainter offset: anOffset [
68
- paragraph paintOn: aCompositorPainter canvas at: anOffset.
69
-
261
+ paragraph paintOn: aCompositorPainter canvas at: anOffset + self padding topLeft.
262
+ self paintSelectionOn: aCompositorPainter offset: anOffset + self padding topLeft.
263
+
70
264
aCompositorPainter
71
265
pushChildren: self children sortedByElevation
72
266
offset: anOffset
@@ -75,3 +269,99 @@ BrTextEditorParagraphElement >> paintChildrenOn: aCompositorPainter offset: anOf
75
269
(aChildElement isKindOf: BlTextElement )
76
270
ifFalse: [ aChildElement fullPaintOn: aChildPainter offset: aChildOffset ] ]
77
271
]
272
+
273
+ { #category : #drawing }
274
+ BrTextEditorParagraphElement >> paintSelectionOn: aCompositorPainter offset: anOffset [
275
+ selection isEmpty
276
+ ifTrue: [ ^ self ].
277
+
278
+ aCompositorPainter canvas transform
279
+ by: [ :t | t translateBy: anOffset ]
280
+ during: [ self drawSelectionOnSpartaCanvas: aCompositorPainter canvas ]
281
+ ]
282
+
283
+ { #category : #private }
284
+ BrTextEditorParagraphElement >> placeholderAndCursorElements [
285
+ ^ self children reject: [ :each | (each isKindOf: BlTextElement ) ]
286
+ ]
287
+
288
+ { #category : #private }
289
+ BrTextEditorParagraphElement >> placeholderElements [
290
+ ^ self children reject: [ :each | (each isKindOf: BlTextElement ) or : [ each isKindOf: BrCursorElement ] ]
291
+ ]
292
+
293
+ { #category : #' api - cursor' }
294
+ BrTextEditorParagraphElement >> removeCursorAt: aTextIndex [
295
+ " Remove cursor at a goven position"
296
+
297
+ ^ self
298
+ cursorAt: aTextIndex
299
+ ifFound: [ :aCursorElement |
300
+ cursorElements remove: aCursorElement.
301
+ aCursorElement removeFromParent.
302
+ aCursorElement ]
303
+ ifNone: [ self error: ' Cursor at ' , aTextIndex asString, ' does not exist' ]
304
+ ]
305
+
306
+ { #category : #' api - cursor' }
307
+ BrTextEditorParagraphElement >> removeCursors [
308
+ " Remove all cursors"
309
+
310
+ self cursorElements reverseDo: [ :eachCursorElement | eachCursorElement removeFromParent ].
311
+ self cursorElements removeAll
312
+ ]
313
+
314
+ { #category : #' api - cursor' }
315
+ BrTextEditorParagraphElement >> screenToCursor: aTransformation at: aPositionInSegment in: aSegment [
316
+ paragraph
317
+ ifNil: [ ^ self ].
318
+
319
+ aTransformation transformed: (aSegment textStart + (paragraph charPositionAt: aPositionInSegment - self padding topLeft))
320
+ ]
321
+
322
+ { #category : #accessing }
323
+ BrTextEditorParagraphElement >> selection [
324
+ ^ selection
325
+ ]
326
+
327
+ { #category : #accessing }
328
+ BrTextEditorParagraphElement >> selection: anObject [
329
+ selection := anObject.
330
+ self invalidate
331
+ ]
332
+
333
+ { #category : #' api - cursor' }
334
+ BrTextEditorParagraphElement >> setCursors: aCollectionOfCursorPositions [
335
+ < return: #BrCursorElement >
336
+ | theExistingCursors theAddedCursors theRemovedCursors |
337
+
338
+ theExistingCursors := self cursorElements collect: [ :eachElement | eachElement textPosition ].
339
+ theAddedCursors := aCollectionOfCursorPositions difference: theExistingCursors.
340
+ theRemovedCursors := theExistingCursors difference: aCollectionOfCursorPositions.
341
+
342
+ theRemovedCursors do: [ :eachToRemove | self removeCursorAt: eachToRemove ].
343
+ theAddedCursors do: [ :eachToAdd | self addCursorAt: eachToAdd ]
344
+ ]
345
+
346
+ { #category : #' api - cursor' }
347
+ BrTextEditorParagraphElement >> showCursors [
348
+ self cursorElements do: [ :aCursorElement |
349
+ aCursorElement visibility: BlVisibility visible.
350
+ aCursorElement hasParent
351
+ ifFalse: [ self addChild: aCursorElement ] ]
352
+ ]
353
+
354
+ { #category : #accessing }
355
+ BrTextEditorParagraphElement >> text [
356
+ ^ text
357
+ ]
358
+
359
+ { #category : #accessing }
360
+ BrTextEditorParagraphElement >> text: aBlText [
361
+ text := aBlText
362
+ ]
363
+
364
+ { #category : #private }
365
+ BrTextEditorParagraphElement >> textElements [
366
+ ^ self children select: [ :each | each isKindOf: BlTextElement ]
367
+ ]
0 commit comments