Skip to content

Commit

Permalink
Support wavy underlines (issue #1131) (#1132)
Browse files Browse the repository at this point in the history
Support wavy underlines, and allow the distance between text and underline to be configured.
  • Loading branch information
shoaniki authored Jul 21, 2022
1 parent 99a5cec commit 9ce8c84
Show file tree
Hide file tree
Showing 4 changed files with 118 additions and 11 deletions.
15 changes: 13 additions & 2 deletions richtextfx/src/main/java/org/fxmisc/richtext/ParagraphText.java
Original file line number Diff line number Diff line change
Expand Up @@ -213,7 +213,9 @@ public ObjectProperty<Paint> highlightTextFillProperty() {
if (attributes.dashArray != null) {
underlineShape.getStrokeDashArray().setAll(attributes.dashArray);
}
underlineShape.getElements().setAll(getUnderlineShape(tuple._2));
PathElement[] shape = getUnderlineShape(tuple._2.getStart(), tuple._2.getEnd(),
attributes.offset, attributes.waveRadius);
underlineShape.getElements().setAll(shape);
},
addToForeground,
clearUnusedShapes
Expand Down Expand Up @@ -597,17 +599,26 @@ public String toString() {
private static class UnderlineAttributes extends LineAttributesBase {

final StrokeLineCap cap;
final double offset;
final double waveRadius;

UnderlineAttributes(TextExt text) {
super(text.getUnderlineColor(), text.getUnderlineWidth(), text.underlineDashArrayProperty());
cap = text.getUnderlineCap();
Number waveNumber = text.getUnderlineWaveRadius();
waveRadius = waveNumber == null ? 0 : waveNumber.doubleValue();
Number offsetNumber = text.getUnderlineOffset();
offset = offsetNumber == null ? waveRadius * 0.5 : offsetNumber.doubleValue();
// The larger the radius the bigger the offset needs to be, so
// a reasonable default is provided if no offset is specified.
}

/**
* Same as {@link #equals(Object)} but no need to check the object for its class
*/
public boolean equalsFaster(UnderlineAttributes attr) {
return super.equalsFaster(attr) && Objects.equals(cap, attr.cap);
return super.equalsFaster(attr) && Objects.equals(cap, attr.cap)
&& offset == attr.offset && waveRadius == attr.waveRadius;
}

@Override
Expand Down
52 changes: 52 additions & 0 deletions richtextfx/src/main/java/org/fxmisc/richtext/TextExt.java
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,8 @@ public class TextExt extends Text {
styleables.add(StyleableProperties.BORDER_DASH_ARRAY);
styleables.add(StyleableProperties.UNDERLINE_COLOR);
styleables.add(StyleableProperties.UNDERLINE_WIDTH);
styleables.add(StyleableProperties.UNDERLINE_OFFSET);
styleables.add(StyleableProperties.UNDERLINE_WAVE_RADIUS);
styleables.add(StyleableProperties.UNDERLINE_DASH_ARRAY);
styleables.add(StyleableProperties.UNDERLINE_CAP);

Expand Down Expand Up @@ -76,6 +78,14 @@ public class TextExt extends Text {
null, "underlineWidth", this, StyleableProperties.UNDERLINE_WIDTH
);

private final StyleableObjectProperty<Number> underlineOffset = new CustomStyleableProperty<>(
null, "underlineOffset", this, StyleableProperties.UNDERLINE_OFFSET
);

private final StyleableObjectProperty<Number> underlineWaveRadius = new CustomStyleableProperty<>(
null, "underlineWaveRadius", this, StyleableProperties.UNDERLINE_WAVE_RADIUS
);

private final StyleableObjectProperty<Number[]> underlineDashArray = new CustomStyleableProperty<>(
null, "underlineDashArray", this, StyleableProperties.UNDERLINE_DASH_ARRAY
);
Expand Down Expand Up @@ -223,6 +233,38 @@ public ObjectProperty<Paint> borderStrokeColorProperty() {
*/
public ObjectProperty<Number> underlineWidthProperty() { return underlineWidth; }

public Number getUnderlineOffset() { return underlineOffset.get(); }
public void setUnderlineOffset(Number width) { underlineOffset.set(width); }

/**
* The offset of the underline for a section of text. If null or zero,
* the underline will be drawn along the baseline of the text.
*
* Can be styled from CSS using the "-rtfx-underline-offset" property.
*
* <p>Note that the underline properties specified here are orthogonal to the {@link #underlineProperty()} inherited
* from {@link Text}. The underline properties defined here in {@link TextExt} will cause an underline to be
* drawn if {@link #underlineWidthProperty()} is non-null and greater than zero, regardless of
* the value of {@link #underlineProperty()}.</p>
*/
public ObjectProperty<Number> underlineOffsetProperty() { return underlineOffset; }

public Number getUnderlineWaveRadius() { return underlineWaveRadius.get(); }
public void setUnderlineWaveRadius(Number radius) { underlineWaveRadius.set(radius); }

/**
* The arc radius used to draw a wavy underline. If null or zero, the
* underline will be a simple line.
*
* Can be styled from CSS using the "-rtfx-underline-wave-radius" property.
*
* <p>Note that the underline properties specified here are orthogonal to the {@link #underlineProperty()} inherited
* from {@link Text}. The underline properties defined here in {@link TextExt} will cause an underline to be
* drawn if {@link #underlineWidthProperty()} is non-null and greater than zero, regardless of
* the value of {@link #underlineProperty()}.</p>
*/
public ObjectProperty<Number> underlineWaveRadiusProperty() { return underlineWaveRadius; }

// Dash array for the text underline
public Number[] getUnderlineDashArray() { return underlineDashArray.get(); }
public void setUnderlineDashArray(Number[] dashArray) { underlineDashArray.set(dashArray); }
Expand Down Expand Up @@ -291,6 +333,16 @@ private static class StyleableProperties {
0, n -> n.underlineWidth
);

private static final CssMetaData<TextExt, Number> UNDERLINE_OFFSET = new CustomCssMetaData<>(
"-rtfx-underline-offset", StyleConverter.getSizeConverter(),
0, n -> n.underlineOffset
);

private static final CssMetaData<TextExt, Number> UNDERLINE_WAVE_RADIUS = new CustomCssMetaData<>(
"-rtfx-underline-wave-radius", StyleConverter.getSizeConverter(),
0, n -> n.underlineWaveRadius
);

private static final CssMetaData<TextExt, Number[]> UNDERLINE_DASH_ARRAY = new CustomCssMetaData<>(
"-rtfx-underline-dash-array", JavaFXCompatibility.SizeConverter_SequenceConverter_getInstance(),
new Double[0], n -> n.underlineDashArray
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -98,13 +98,19 @@ PathElement[] getUnderlineShape(IndexRange range) {
return getUnderlineShape(range.getStart(), range.getEnd());
}

PathElement[] getUnderlineShape(int from, int to) {
return getUnderlineShape(from, to, 0, 0);
}

/**
* @param from The index of the first character.
* @param to The index of the last character.
* @param offset Ignored (only implemented for Java 9+)
* @param waveRadius Ignored (only implemented for Java 9+)
* @return An array with the PathElement objects which define an
* underline from the first to the last character.
*/
PathElement[] getUnderlineShape(int from, int to) {
PathElement[] getUnderlineShape(int from, int to, double offset, double waveRadius) {
// get a Path for the text underline
PathElement[] shape = textLayout().getRange(from, to, TextLayout.TYPE_UNDERLINE, 0, 0);

Expand Down
54 changes: 46 additions & 8 deletions richtextfx/src/main/java9/org/fxmisc/richtext/TextFlowExt.java
Original file line number Diff line number Diff line change
Expand Up @@ -13,8 +13,7 @@
import javafx.scene.shape.PathElement;
import javafx.scene.text.HitInfo;
import javafx.scene.text.TextFlow;
import javafx.scene.shape.LineTo;
import javafx.scene.shape.MoveTo;
import javafx.scene.shape.*;

/**
* Adds additional API to {@link TextFlow}.
Expand Down Expand Up @@ -69,13 +68,19 @@ PathElement[] getUnderlineShape(IndexRange range) {
return getUnderlineShape(range.getStart(), range.getEnd());
}

PathElement[] getUnderlineShape(int from, int to) {
return getUnderlineShape(from, to, 0, 0);
}

/**
* @param from The index of the first character.
* @param to The index of the last character.
* @param offset The distance below the baseline to draw the underline.
* @param waveRadius If non-zero, draw a wavy underline with arcs of this radius.
* @return An array with the PathElement objects which define an
* underline from the first to the last character.
*/
PathElement[] getUnderlineShape(int from, int to) {
PathElement[] getUnderlineShape(int from, int to, double offset, double waveRadius) {
// get a Path for the text underline
List<PathElement> result = new ArrayList<>();

Expand All @@ -88,10 +93,43 @@ PathElement[] getUnderlineShape(int from, int to) {
{
LineTo bl = (LineTo) shape[ele+1];
LineTo br = (LineTo) shape[ele];
double y = br.getY() - 2.5;

result.add( new MoveTo( bl.getX(), y ) );
result.add( new LineTo( br.getX(), y ) );

double y = snapSizeY( br.getY() + offset - 2.5 );
double leftx = snapSizeX( bl.getX() );

if (waveRadius <= 0) {
result.add(new MoveTo( leftx, y ));
result.add(new LineTo( snapSizeX( br.getX() ), y ));
}
else {
// For larger wave radii increase the X radius to stretch out the wave.
double radiusX = waveRadius > 1 ? waveRadius * 1.25 : waveRadius;
double rightx = br.getX();
result.add(new MoveTo( leftx, y ));
boolean sweep = true;
while ( leftx < rightx ) {
leftx += waveRadius * 2;

if (leftx > rightx) {
// Since we are drawing the wave in segments, it is necessary to
// clip the final arc to avoid over/underflow with larger radii,
// so we must compute the y value for the point on the arc where
// x = rightx.
// To simplify the computation, we translate so that the center of
// the arc has x = 0, and the known endpoints have y = 0.
double dx = rightx - (leftx - waveRadius);
double dxsq = dx * dx;
double rxsq = radiusX * radiusX;
double rysq = waveRadius * waveRadius;
double dy = waveRadius * (Math.sqrt(1 - dxsq/rxsq) - Math.sqrt(1 - rysq/rxsq));

if (sweep) y -= dy; else y += dy;
leftx = rightx;
}
result.add(new ArcTo( radiusX, waveRadius, 0.0, leftx, y, false, sweep ));
sweep = !sweep;
}
}
}

return result.toArray(new PathElement[0]);
Expand Down Expand Up @@ -134,4 +172,4 @@ CharacterHit hit(double x, double y) {
}
}

}
}

0 comments on commit 9ce8c84

Please sign in to comment.