11package org .fxmisc .richtext ;
22
33import java .util .ArrayList ;
4+ import java .util .Arrays ;
5+ import java .util .LinkedList ;
46import java .util .List ;
7+ import java .util .Objects ;
58import java .util .Optional ;
9+ import java .util .function .BiConsumer ;
610import java .util .function .Function ;
11+ import java .util .function .Predicate ;
12+ import java .util .function .Supplier ;
13+ import java .util .function .UnaryOperator ;
714
815import javafx .beans .property .ObjectProperty ;
916import javafx .beans .property .SimpleObjectProperty ;
17+ import javafx .collections .ObservableList ;
1018import javafx .collections .transformation .FilteredList ;
1119import javafx .geometry .Bounds ;
1220import javafx .geometry .Insets ;
1927import javafx .scene .shape .StrokeLineCap ;
2028
2129import org .fxmisc .richtext .model .Paragraph ;
30+ import org .reactfx .util .Tuple2 ;
31+ import org .reactfx .util .Tuples ;
2232import org .reactfx .value .Val ;
2333import org .reactfx .value .Var ;
2434
@@ -45,8 +55,11 @@ public ObjectProperty<Paint> highlightTextFillProperty() {
4555
4656 private final Path caretShape = new Path ();
4757 private final Path selectionShape = new Path ();
48- private final List <Path > backgroundShapes ;
49- private final List <Path > underlineShapes ;
58+ private final List <Path > backgroundShapes = new LinkedList <>();
59+ private final List <Path > underlineShapes = new LinkedList <>();
60+
61+ private final List <Tuple2 <Paint , IndexRange >> backgroundColorRanges = new LinkedList <>();
62+ private final List <Tuple2 <UnderlineAttributes , IndexRange >> underlineRanges = new LinkedList <>();
5063 private final Val <Double > leftInset ;
5164 private final Val <Double > topInset ;
5265
@@ -97,20 +110,12 @@ public ObjectProperty<Paint> highlightTextFillProperty() {
97110// text.impl_selectionFillProperty().set(newFill);
98111// }
99112// });
100- int size = par .getSegments ().size ();
101- backgroundShapes = new ArrayList <>(size );
102- underlineShapes = new ArrayList <>(size );
103113
104114 // populate with text nodes
105115 for (SEG segment : par .getSegments ()) {
106116 // create Segment
107117 Node fxNode = nodeFactory .apply (segment );
108118 getChildren ().add (fxNode );
109-
110- // add placeholder to prevent IOOBE; only create shapes when needed
111- backgroundShapes .add (null );
112- underlineShapes .add (null );
113-
114119 }
115120 }
116121
@@ -197,130 +202,112 @@ private void updateSelectionShape() {
197202 }
198203
199204 private void updateBackgroundShapes () {
200- int index = 0 ;
201205 int start = 0 ;
202206
207+ // calculate shared values among consecutive nodes
203208 FilteredList <Node > nodeList = getChildren ().filtered (node -> node instanceof TextExt );
204209 for (Node node : nodeList ) {
205210 TextExt text = (TextExt ) node ;
206211 int end = start + text .getText ().length ();
207212
208- updateBackground (text , start , end , index );
209- updateUnderline (text , start , end , index );
213+ Paint backgroundColor = text .getBackgroundColor ();
214+ if (backgroundColor != null ) {
215+ updateSharedShapeRange (backgroundColorRanges , backgroundColor , start , end );
216+ }
217+
218+ UnderlineAttributes attributes = new UnderlineAttributes (text );
219+ if (!attributes .isNullValue ()) {
220+ updateSharedShapeRange (underlineRanges , attributes , start , end );
221+ }
210222
211223 start = end ;
212- index ++;
213224 }
214- }
215225
216- private Path getBackgroundShape (int index ) {
217- Path backgroundShape = backgroundShapes .get (index );
218- if (backgroundShape == null ) {
219- // add corresponding background node (empty)
220- backgroundShape = new Path ();
221- backgroundShape .setManaged (false );
222- backgroundShape .setStrokeWidth (0 );
223- backgroundShape .layoutXProperty ().bind (leftInset );
224- backgroundShape .layoutYProperty ().bind (topInset );
225- backgroundShapes .set (index , backgroundShape );
226- getChildren ().add (0 , backgroundShape );
227- }
228- return backgroundShape ;
226+ // now only use one shape per shared value
227+ updateSharedShapes (backgroundColorRanges , backgroundShapes , (children , shape ) -> children .add (0 , shape ),
228+ (colorShape , tuple ) -> {
229+ colorShape .setStrokeWidth (0 );
230+ colorShape .setFill (tuple ._1 );
231+ colorShape .getElements ().setAll (getRangeShape (tuple ._2 ));
232+ });
233+ updateSharedShapes (underlineRanges , underlineShapes , (children , shape ) -> children .add (shape ),
234+ (underlineShape , tuple ) -> {
235+ UnderlineAttributes attributes = tuple ._1 ;
236+ underlineShape .setStroke (attributes .color );
237+ underlineShape .setStrokeWidth (attributes .width );
238+ underlineShape .setStrokeLineCap (attributes .cap );
239+ if (attributes .dashArray != null ) {
240+ underlineShape .getStrokeDashArray ().setAll (attributes .dashArray );
241+ }
242+ underlineShape .getElements ().setAll (getUnderlineShape (tuple ._2 ));
243+ });
229244 }
230245
231246 /**
232- * Updates the background shape for a text segment.
233- *
234- * @param text The text node which specified the style attributes
235- * @param start The index of the first character
236- * @param end The index of the last character
237- * @param index The index of the background shape
247+ * Calculates the range of a value (background color, underline, etc.) that is shared between multiple
248+ * consecutive {@link TextExt} nodes
238249 */
239- private void updateBackground (TextExt text , int start , int end , int index ) {
240- // Set fill
241- Paint paint = text .backgroundColorProperty ().get ();
242- if (paint != null ) {
243- Path backgroundShape = getBackgroundShape (index );
244- backgroundShape .setFill (paint );
245-
246- // Set path elements
247- PathElement [] shape = getRangeShape (start , end );
248- backgroundShape .getElements ().setAll (shape );
249- }
250+ private <T > void updateSharedShapeRange (List <Tuple2 <T , IndexRange >> rangeList , T value , int start , int end ) {
251+ updateSharedShapeRange0 (
252+ rangeList ,
253+ () -> Tuples .t (value , new IndexRange (start , end )),
254+ lastRange -> {
255+ T lastShapeValue = lastRange ._1 ;
256+ return lastShapeValue .equals (value );
257+ },
258+ lastRange -> lastRange .map ((val , range ) -> Tuples .t (val , new IndexRange (range .getStart (), end )))
259+ );
250260 }
251261
252- private Path getUnderlineShape (int index ) {
253- Path underlineShape = underlineShapes .get (index );
254- if (underlineShape == null ) {
255- // add corresponding underline node (empty)
256- underlineShape = new Path ();
257- underlineShape .setManaged (false );
258- underlineShape .setStrokeWidth (0 );
259- underlineShape .layoutXProperty ().bind (leftInset );
260- underlineShape .layoutYProperty ().bind (topInset );
261- underlineShapes .set (index , underlineShape );
262- getChildren ().add (underlineShape );
262+ private <T > void updateSharedShapeRange0 (List <T > rangeList , Supplier <T > newValueRange ,
263+ Predicate <T > sharesShapeValue , UnaryOperator <T > mapper ) {
264+ if (rangeList .isEmpty ()) {
265+ rangeList .add (newValueRange .get ());
266+ } else {
267+ int lastIndex = rangeList .size () - 1 ;
268+ T lastShapeValueRange = rangeList .get (lastIndex );
269+ if (sharesShapeValue .test (lastShapeValueRange )) {
270+ rangeList .set (lastIndex , mapper .apply (lastShapeValueRange ));
271+ } else {
272+ rangeList .add (newValueRange .get ());
273+ }
263274 }
264- return underlineShape ;
265275 }
266276
267277 /**
268- * Updates the shape which renders the text underline.
269- *
270- * @param text The text node which specified the style attributes
271- * @param start The index of the first character
272- * @param end The index of the last character
273- * @param index The index of the background shape
278+ * Updates the shapes calculated in {@link #updateSharedShapeRange(List, Object, int, int)} and configures them
279+ * via {@code configureShape}.
274280 */
275- private void updateUnderline (TextExt text , int start , int end , int index ) {
276-
277- Number underlineWidth = text .underlineWidthProperty ().get ();
278- if (underlineWidth != null && underlineWidth .doubleValue () > 0 ) {
279-
280- Path underlineShape = getUnderlineShape (index );
281- underlineShape .setStrokeWidth (underlineWidth .doubleValue ());
282-
283- // get remaining CSS properties for the underline style
284-
285- Paint underlineColor = text .underlineColorProperty ().get ();
286-
287- // get the dash array - JavaFX CSS parser seems to return either a Number[] array
288- // or a single value, depending on whether only one or more than one value has been
289- // specified in the CSS
290- Double [] underlineDashArray = null ;
291- Object underlineDashArrayProp = text .underlineDashArrayProperty ().get ();
292- if (underlineDashArrayProp != null ) {
293- if (underlineDashArrayProp .getClass ().isArray ()) {
294- Number [] numberArray = (Number []) underlineDashArrayProp ;
295- underlineDashArray = new Double [numberArray .length ];
296- int idx = 0 ;
297- for (Number d : numberArray ) {
298- underlineDashArray [idx ++] = (Double ) d ;
299- }
300- } else {
301- underlineDashArray = new Double [1 ];
302- underlineDashArray [0 ] = ((Double ) underlineDashArrayProp ).doubleValue ();
303- }
304- }
305-
306- StrokeLineCap underlineCap = text .underlineCapProperty ().get ();
307-
308- // apply style
309- if (underlineColor != null ) {
310- underlineShape .setStroke (underlineColor );
311- }
312- if (underlineDashArray != null ) {
313- underlineShape .getStrokeDashArray ().addAll (underlineDashArray );
314- }
315- if (underlineCap != null ) {
316- underlineShape .setStrokeLineCap (underlineCap );
281+ private <T > void updateSharedShapes (List <T > rangeList , List <Path > shapeList ,
282+ BiConsumer <ObservableList <Node >, Path > addToChildren ,
283+ BiConsumer <Path , T > configureShape ) {
284+ // remove or add shapes, depending on what's needed
285+ int neededNumber = rangeList .size ();
286+ int availableNumber = shapeList .size ();
287+
288+ if (neededNumber < availableNumber ) {
289+ List <Path > unusedShapes = shapeList .subList (neededNumber , availableNumber );
290+ getChildren ().removeAll (unusedShapes );
291+ unusedShapes .clear ();
292+ } else if (availableNumber < neededNumber ) {
293+ for (int i = 0 ; i < neededNumber - availableNumber ; i ++) {
294+ Path shape = new Path ();
295+ shape .setManaged (false );
296+ shape .layoutXProperty ().bind (leftInset );
297+ shape .layoutYProperty ().bind (topInset );
298+
299+ shapeList .add (shape );
300+ addToChildren .accept (getChildren (), shape );
317301 }
302+ }
318303
319- // Set path elements
320- PathElement [] shape = getUnderlineShape ( start , end );
321- underlineShape . getElements (). setAll ( shape );
304+ // update the shape's color and elements
305+ for ( int i = 0 ; i < rangeList . size (); i ++) {
306+ configureShape . accept ( shapeList . get ( i ), rangeList . get ( i ) );
322307 }
323308
309+ // clear, since it's no longer needed
310+ rangeList .clear ();
324311 }
325312
326313
@@ -331,4 +318,67 @@ protected void layoutChildren() {
331318 updateSelectionShape ();
332319 updateBackgroundShapes ();
333320 }
321+
322+ private static class UnderlineAttributes {
323+
324+ private final double width ;
325+ private final Paint color ;
326+ private final Double [] dashArray ;
327+ private final StrokeLineCap cap ;
328+
329+ public final boolean isNullValue () { return color == null || width == -1 ; }
330+
331+ UnderlineAttributes (TextExt text ) {
332+ color = text .getUnderlineColor ();
333+ Number underlineWidth = text .getUnderlineWidth ();
334+ if (color == null || underlineWidth == null || underlineWidth .doubleValue () <= 0 ) {
335+ // null value
336+ width = -1 ;
337+ dashArray = null ;
338+ cap = null ;
339+ } else {
340+ // real value
341+ width = underlineWidth .doubleValue ();
342+ cap = text .getUnderlineCap ();
343+
344+ // get the dash array - JavaFX CSS parser seems to return either a Number[] array
345+ // or a single value, depending on whether only one or more than one value has been
346+ // specified in the CSS
347+ Object underlineDashArrayProp = text .underlineDashArrayProperty ().get ();
348+ if (underlineDashArrayProp != null ) {
349+ if (underlineDashArrayProp .getClass ().isArray ()) {
350+ Number [] numberArray = (Number []) underlineDashArrayProp ;
351+ dashArray = new Double [numberArray .length ];
352+ int idx = 0 ;
353+ for (Number d : numberArray ) {
354+ dashArray [idx ++] = (Double ) d ;
355+ }
356+ } else {
357+ dashArray = new Double [1 ];
358+ dashArray [0 ] = ((Double ) underlineDashArrayProp ).doubleValue ();
359+ }
360+ } else {
361+ dashArray = null ;
362+ }
363+ }
364+ }
365+
366+ @ Override
367+ public boolean equals (Object obj ) {
368+ if (obj instanceof UnderlineAttributes ) {
369+ UnderlineAttributes attr = (UnderlineAttributes ) obj ;
370+ return Objects .equals (width , attr .width )
371+ && Objects .equals (color , attr .color )
372+ && Objects .equals (cap , attr .cap )
373+ && Arrays .equals (dashArray , attr .dashArray );
374+ } else {
375+ return false ;
376+ }
377+ }
378+
379+ @ Override
380+ public String toString () {
381+ return String .format ("UnderlineAttributes[width=%s color=%s cap=%s dashArray=%s" , width , color , cap , Arrays .toString (dashArray ));
382+ }
383+ }
334384}
0 commit comments