Skip to content

Commit b366da3

Browse files
Merge pull request #558 from JordanMartinez/optimizeBackgroundColorShapes
Optimize background color and underline shapes
2 parents 542786c + 4cca5e0 commit b366da3

2 files changed

Lines changed: 167 additions & 108 deletions

File tree

richtextfx/src/main/java/org/fxmisc/richtext/ParagraphText.java

Lines changed: 158 additions & 108 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,20 @@
11
package org.fxmisc.richtext;
22

33
import java.util.ArrayList;
4+
import java.util.Arrays;
5+
import java.util.LinkedList;
46
import java.util.List;
7+
import java.util.Objects;
58
import java.util.Optional;
9+
import java.util.function.BiConsumer;
610
import java.util.function.Function;
11+
import java.util.function.Predicate;
12+
import java.util.function.Supplier;
13+
import java.util.function.UnaryOperator;
714

815
import javafx.beans.property.ObjectProperty;
916
import javafx.beans.property.SimpleObjectProperty;
17+
import javafx.collections.ObservableList;
1018
import javafx.collections.transformation.FilteredList;
1119
import javafx.geometry.Bounds;
1220
import javafx.geometry.Insets;
@@ -19,6 +27,8 @@
1927
import javafx.scene.shape.StrokeLineCap;
2028

2129
import org.fxmisc.richtext.model.Paragraph;
30+
import org.reactfx.util.Tuple2;
31+
import org.reactfx.util.Tuples;
2232
import org.reactfx.value.Val;
2333
import 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
}

richtextfx/src/main/java/org/fxmisc/richtext/TextFlowExt.java

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77
import java.util.ArrayList;
88
import java.util.List;
99

10+
import javafx.scene.control.IndexRange;
1011
import org.fxmisc.richtext.model.TwoLevelNavigator;
1112

1213
import javafx.scene.shape.PathElement;
@@ -89,10 +90,18 @@ PathElement[] getCaretShape(int charIdx, boolean isLeading) {
8990
return textLayout().getCaretShape(charIdx, isLeading, 0.0f, 0.0f);
9091
}
9192

93+
PathElement[] getRangeShape(IndexRange range) {
94+
return getRangeShape(range.getStart(), range.getEnd());
95+
}
96+
9297
PathElement[] getRangeShape(int from, int to) {
9398
return textLayout().getRange(from, to, TextLayout.TYPE_TEXT, 0, 0);
9499
}
95100

101+
PathElement[] getUnderlineShape(IndexRange range) {
102+
return getUnderlineShape(range.getStart(), range.getEnd());
103+
}
104+
96105
/**
97106
* @param from The index of the first character.
98107
* @param to The index of the last character.

0 commit comments

Comments
 (0)