Skip to content

Commit 36b61ce

Browse files
committedAug 27, 2020
Merge bf21369
2 parents 004ba70 + bf21369 commit 36b61ce

4 files changed

+465
-51
lines changed
 

‎src/Brick-Editor/BrTextEditorParagraphElement.class.st

+310-20
Original file line numberDiff line numberDiff line change
@@ -2,71 +2,265 @@ Class {
22
#name : #BrTextEditorParagraphElement,
33
#superclass : #BlElement,
44
#instVars : [
5-
'paragraph'
5+
'paragraph',
6+
'text',
7+
'cursorElements',
8+
'cursorStencil',
9+
'selection'
10+
],
11+
#classInstVars : [
12+
'aCompositorPainter'
613
],
714
#category : #'Brick-Editor-UI'
815
}
916

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' }
1178
BrTextEditorParagraphElement >> buildParagraph [
1279
| aBuilder |
80+
1381
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))
1583
fontCollection: (SkiaFontCollection new defaultFontManager: SkiaFontManager default).
1684

1785
self childrenDo: [ :eachElement |
1886
(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+
2292
^ aBuilder build
2393
]
2494

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+
25136
{ #category : #drawing }
26137
BrTextEditorParagraphElement >> drawChildrenOnSpartaCanvas: aCanvas [
27138
aCanvas clip
28139
when: [ self clipChildren ]
29140
by: [ self geometry pathOnSpartaCanvas: aCanvas of: self ]
30141
during: [
31-
paragraph paintOn: aCanvas at: 0@0.
142+
paragraph paintOn: aCanvas at: self padding topLeft.
32143
self children sortedByElevation
33-
do: [ :anElement |
144+
do: [ :anElement |
34145
(anElement isKindOf: BlTextElement)
35146
ifFalse: [ anElement fullDrawOnSpartaCanvas: aCanvas ] ] ]
36147
]
37148

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+
38226
{ #category : #'hooks - children' }
39227
BrTextEditorParagraphElement >> onChildAdded: anElement [
40228
super onChildAdded: anElement.
41229

42-
paragraph := nil
230+
(anElement isKindOf: BrCursorElement)
231+
ifFalse: [ paragraph := nil ]
232+
233+
43234
]
44235

45-
{ #category : #'hooks - children' }
236+
{ #category : #layout }
46237
BrTextEditorParagraphElement >> onLayout: aBounds [
47238
| placeholders |
48239

49-
placeholders := self children reject: [ :each | each isKindOf: BlTextElement ].
240+
placeholders := self placeholderElements.
50241
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 ].
52245
]
53246

54-
{ #category : #'hooks - children' }
247+
{ #category : #layout }
55248
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 ].
59251

60252
paragraph := self buildParagraph.
61253
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 ]
64257
]
65258

66259
{ #category : #drawing }
67260
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+
70264
aCompositorPainter
71265
pushChildren: self children sortedByElevation
72266
offset: anOffset
@@ -75,3 +269,99 @@ BrTextEditorParagraphElement >> paintChildrenOn: aCompositorPainter offset: anOf
75269
(aChildElement isKindOf: BlTextElement)
76270
ifFalse: [ aChildElement fullPaintOn: aChildPainter offset: aChildOffset ] ]
77271
]
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

Comments
 (0)