package de.lessvoid.nifty.elements.render;
import java.util.ArrayList;
import java.util.List;
import javax.annotation.Nonnull;
import javax.annotation.Nullable;
import org.bushe.swing.event.EventSubscriber;
import de.lessvoid.nifty.Nifty;
import de.lessvoid.nifty.NiftyLocaleChangedEvent;
import de.lessvoid.nifty.elements.Element;
import de.lessvoid.nifty.elements.tools.FontHelper;
import de.lessvoid.nifty.elements.tools.TextBreak;
import de.lessvoid.nifty.layout.align.HorizontalAlign;
import de.lessvoid.nifty.layout.align.VerticalAlign;
import de.lessvoid.nifty.render.NiftyRenderEngine;
import de.lessvoid.nifty.spi.render.RenderFont;
import de.lessvoid.nifty.tools.Color;
import de.lessvoid.nifty.tools.SizeValue;
/**
* The TextRenderer implementation.
*
* @author void
*/
public class TextRenderer implements ElementRenderer, EventSubscriber<NiftyLocaleChangedEvent> {
/**
* The default color used by the renderer.
*/
@Nonnull
public static final Color DEFAULT_COLOR = Color.WHITE;
/**
* the font to use.
*/
@Nullable
private RenderFont font;
/**
* this is the original text.
*/
@Nonnull
private String originalText;
/**
* the text to output.
*/
@Nullable
private String[] textLines;
/**
* max width of all text strings.
*/
private int maxWidth;
/**
* can't remember what this is :>.
*/
private int xOffsetHack = 0;
/**
* selection start.
*/
private int selectionStart = -1;
/**
* selection end.
*/
private int selectionEnd = -1;
/**
* text selection color.
*/
@Nonnull
private Color textSelectionColor = Color.BLACK;
/**
* vertical alignment.
*/
@Nonnull
private VerticalAlign textVAlign = VerticalAlign.center;
/**
* horizontal alignment.
*/
@Nonnull
private HorizontalAlign textHAlign = HorizontalAlign.center;
/**
* color.
*/
@Nonnull
private Color color = DEFAULT_COLOR;
/**
* This TextRenderer will automatically wrap lines when the element it is
* attached to has a width constraint.
*/
private boolean lineWrapping = false;
/**
* If the textLineHeight property is set it will override the font.getHeight() when
* calculating the height of the text.
*/
@Nonnull
private SizeValue textLineHeight = SizeValue.def();
/**
* If the textMinLineHeight property is set the text will always be at least textMinLineHeight
* pixel height.
*/
@Nonnull
private SizeValue textMinHeight = SizeValue.def();
@Nonnull
private final Nifty nifty;
// in case we use word wrapping and are changing the elements width/height constraints we'll
// remember the original values in here
private boolean isCalculatedLineWrapping = false;
@Nonnull
private SizeValue originalConstraintWidth = SizeValue.def();
@Nonnull
private SizeValue originalConstraintHeight = SizeValue.def();
/*
* When the element this TextRenderer belongs to as been layouted at least once we remember the attached element
* here so that we can later automatically relayout ourself correctly when someone changed this text.
*/
@Nullable
private Element hasBeenLayoutedElement;
private String originalTextBeforeSpecialValues;
/**
* default constructor.
*/
public TextRenderer(@Nonnull final Nifty nifty) {
this.nifty = nifty;
this.nifty.getEventService().subscribe(NiftyLocaleChangedEvent.class, this);
originalText = "";
}
/**
* create new renderer with the given font and text.
*
* @param newFont the font to use
* @param newText the text to use
*/
public TextRenderer(@Nonnull final Nifty nifty, @Nonnull final RenderFont newFont, @Nullable final String newText) {
this.nifty = nifty;
this.nifty.getEventService().subscribe(NiftyLocaleChangedEvent.class, this);
init(newFont, newText);
}
/**
* set Text.
*
* @param newText text
*/
public void setText(@Nullable final String newText) {
initText(newText, true);
}
/**
* initialize.
*
* @param newFont new font
* @param newText new text
*/
private void init(@Nonnull final RenderFont newFont, @Nullable final String newText) {
this.font = newFont;
initText(newText, false);
}
/**
* @param text the text that is supposed to be used
*/
private void initText(@Nullable final String text, final boolean changeExistingText) {
this.originalTextBeforeSpecialValues = text;
String newText = nifty.specialValuesReplace(text);
if (lineWrapping && isCalculatedLineWrapping) {
isCalculatedLineWrapping = false;
}
this.originalText = newText;
this.textLines = newText.split("\n", -1);
if (changeExistingText && hasBeenLayoutedElement != null) {
hasBeenLayoutedElement.getParent().layoutElements();
}
maxWidth = 0;
if (font != null) {
for (int i = 0; i < textLines.length; i++) {
String line = textLines[i];
int lineWidth = font.getWidth(line);
if (lineWidth > maxWidth) {
maxWidth = lineWidth;
}
}
}
}
/**
* render the stuff.
*
* @param w the widget we're connected to
* @param r the renderDevice we should use
*/
@Override
public void render(@Nonnull final Element w, @Nonnull final NiftyRenderEngine r) {
if (textLines == null) {
return;
}
renderLines(w, r, textLines);
}
private void renderLines(@Nonnull final Element w, @Nonnull final NiftyRenderEngine r, @Nonnull String... lines) {
RenderFont font = ensureFont(r);
if (font == null) {
return;
}
boolean stateSaved = prepareRenderEngine(r, font);
int y = getStartYWithVerticalAlign(lines.length * font.getHeight(), w.getHeight(), textVAlign);
for (String line : lines) {
int yy = w.getY() + y;
if (Math.abs(xOffsetHack) > 0) {
int fittingOffset = FontHelper.getVisibleCharactersFromStart(font, line, Math.abs(xOffsetHack), 1.0f);
String cut = line.substring(0, fittingOffset);
String substring = line.substring(fittingOffset, line.length());
int xx = w.getX() + xOffsetHack + font.getWidth(cut);
renderLine(xx, yy, substring, r, selectionStart - fittingOffset, selectionEnd - fittingOffset);
} else {
int xx = w.getX() + getStartXWithHorizontalAlign(font.getWidth(line), w.getWidth(), textHAlign);
renderLine(xx, yy, line, r, selectionStart, selectionEnd);
}
y += font.getHeight();
}
restoreRenderEngine(r, stateSaved);
}
private boolean prepareRenderEngine(@Nonnull final NiftyRenderEngine r, RenderFont font) {
if (!r.isColorChanged()) {
if (r.isColorAlphaChanged()) {
r.setColorIgnoreAlpha(color);
} else {
r.setColor(color);
}
}
boolean stateSaved = false;
if (r.getFont() == null) {
r.saveStates();
r.setFont(font);
stateSaved = true;
}
return stateSaved;
}
private void restoreRenderEngine(@Nonnull final NiftyRenderEngine r, final boolean stateSaved) {
if (stateSaved) {
r.restoreStates();
}
}
@Nullable
private RenderFont ensureFont(@Nonnull final NiftyRenderEngine r) {
if (this.font == null) {
return r.getFont();
}
return font;
}
/**
* Get start Y for text rendering given the textHeight and the elementHeight.
*
* @param textHeight text height
* @param elementHeight element height
* @param verticalAlign verticalAlign
* @return start y for text rendering
*/
protected static int getStartYWithVerticalAlign(
final int textHeight,
final int elementHeight,
final VerticalAlign verticalAlign) {
if (VerticalAlign.top == verticalAlign) {
return 0;
} else if (VerticalAlign.center == verticalAlign) {
return (elementHeight - textHeight) / 2;
} else if (VerticalAlign.bottom == verticalAlign) {
return elementHeight - textHeight;
} else {
// default is top in here
return 0;
}
}
/**
* Get start x for text rendering given the textWidth and the elementWidth.
*
* @param textWidth text width
* @param elementWidth element width
* @param horizontalAlign horizontalAlign
* @return start x for text rendering
*/
protected static int getStartXWithHorizontalAlign(
final int textWidth,
final int elementWidth,
final HorizontalAlign horizontalAlign) {
if (HorizontalAlign.left == horizontalAlign) {
return 0;
} else if (HorizontalAlign.center == horizontalAlign) {
return (elementWidth - textWidth) / 2;
} else if (HorizontalAlign.right == horizontalAlign) {
return elementWidth - textWidth;
} else {
// default is 0
return 0;
}
}
/**
* render line.
*
* @param xx x
* @param yy y
* @param line line
* @param r RenderEngine
* @param selStart sel start
* @param selEnd sel end
*/
private void renderLine(
final int xx,
final int yy,
@Nonnull final String line,
@Nonnull final NiftyRenderEngine r,
final int selStart,
final int selEnd) {
r.renderText(line, xx, yy, selStart, selEnd, textSelectionColor);
}
/**
* Helper method to get width of text.
*
* @return the width in pixel of the current set text.
*/
public int getTextWidth() {
return maxWidth;
}
/**
* Helper method to get height of text.
*
* @return the height in pixel of the current set text.
*/
public int getTextHeight() {
RenderFont font = ensureFont(nifty.getRenderEngine());
if (font == null || textLines == null) {
return 0;
}
int calculatedHeight = font.getHeight() * textLines.length;
if (textLineHeight.hasValue()) {
calculatedHeight = textLineHeight.getValueAsInt(1.0f) * textLines.length;
}
if (textMinHeight.hasValue()) {
if (calculatedHeight < textMinHeight.getValueAsInt(1.0f)) {
return textMinHeight.getValueAsInt(1.0f);
}
}
return calculatedHeight;
}
/**
* set thing.
*
* @param newXoffsetHack xoffset
*/
public void setxOffsetHack(final int newXoffsetHack) {
this.xOffsetHack = newXoffsetHack;
}
/**
* Get RenderFont.
*
* @return render font
*/
@Nullable
public RenderFont getFont() {
return font;
}
/**
* set a new selection.
*
* @param selectionStartParam start
* @param selectionEndParam end
*/
public void setSelection(final int selectionStartParam, final int selectionEndParam) {
this.selectionStart = selectionStartParam;
this.selectionEnd = selectionEndParam;
}
/**
* Set the font that is supposed to be used.
*
* @param fontParam the font or {@code null} in case the font of the render engine is supposed to be used
*/
public void setFont(@Nullable final RenderFont fontParam) {
this.font = fontParam;
}
/**
* set new text selection color.
*
* @param textSelectionColorParam text selection color
*/
public void setTextSelectionColor(@Nonnull final Color textSelectionColorParam) {
this.textSelectionColor = textSelectionColorParam;
}
/**
* set text vertical alignment.
*
* @param newTextVAlign text vertical alignment
*/
public void setTextVAlign(@Nonnull final VerticalAlign newTextVAlign) {
this.textVAlign = newTextVAlign;
}
/**
* set text horizontal alignment.
*
* @param newTextHAlign text horizontal alignment
*/
public void setTextHAlign(@Nonnull final HorizontalAlign newTextHAlign) {
this.textHAlign = newTextHAlign;
}
/**
* set color.
*
* @param newColor color
*/
public void setColor(@Nonnull final Color newColor) {
this.color = newColor;
}
/**
* get original text.
*
* @return original text
*/
@Nonnull
public String getOriginalText() {
return originalText;
}
@Nonnull
public String getWrappedText() {
StringBuilder result = new StringBuilder();
if (textLines != null && textLines.length > 0) {
result.append(textLines[0]);
for (int i = 1; i < textLines.length; i++) {
result.append('\n').append(textLines[i]);
}
}
return result.toString();
}
public void setTextLineHeight(@Nonnull final SizeValue textLineHeight) {
this.textLineHeight = textLineHeight;
}
public void setTextMinHeight(@Nonnull final SizeValue textMinHeight) {
this.textMinHeight = textMinHeight;
}
@Nonnull
private String[] wrapText(final int width, @Nonnull final NiftyRenderEngine r, @Nonnull final String... textLines) {
RenderFont font = ensureFont(r);
if (font == null) {
return textLines;
}
List<String> lines = new ArrayList<String>();
for (String line : textLines) {
int lineLengthInPixel = font.getWidth(line);
if (lineLengthInPixel > width) {
lines.addAll(new TextBreak(line, width, font).split());
} else {
lines.add(line);
}
}
return lines.toArray(new String[lines.size()]);
}
public void setWidthConstraint(
@Nonnull final Element element,
@Nonnull final SizeValue elementConstraintWidth,
final int parentWidth,
@Nonnull final NiftyRenderEngine renderEngine) {
if (parentWidth == 0 || !lineWrapping || isCalculatedLineWrapping) {
return;
}
int valueAsInt = element.getWidth();
if (valueAsInt == 0) {
valueAsInt = elementConstraintWidth.getValueAsInt(parentWidth);
}
if (valueAsInt <= 0) {
return;
}
// remember some values so that we can correctly do auto word wrapping when someone changes the text
this.hasBeenLayoutedElement = element;
this.textLines = wrapText(valueAsInt, renderEngine, originalText.split("\n", -1));
maxWidth = valueAsInt;
// we'll now modify the element constraints so that the layout mechanism can later take this word wrapping
// business correctly into account when the elements will be layouted. to make sure we're able to reset this
// effect later, we'll remember that we've artificially calculated those values in here. so that we're able to
// actually reset this later.
isCalculatedLineWrapping = true;
originalConstraintWidth = element.getConstraintWidth();
originalConstraintHeight = element.getConstraintHeight();
element.setConstraintWidth(
elementConstraintWidth.hasWildcard() ? SizeValue.wildcard(getTextWidth()) : SizeValue.px(getTextWidth()));
element.setConstraintHeight(SizeValue.px(getTextHeight()));
}
public void setLineWrapping(final boolean lineWrapping) {
this.lineWrapping = lineWrapping;
}
public boolean isLineWrapping() {
return lineWrapping;
}
@Nonnull
public VerticalAlign getTextVAlign() {
return textVAlign;
}
@Nonnull
public HorizontalAlign getTextHAlign() {
return textHAlign;
}
@Nonnull
public Color getColor() {
return color;
}
public void resetLayout(@Nonnull final Element element) {
if (isCalculatedLineWrapping) {
isCalculatedLineWrapping = false;
element.setConstraintWidth(originalConstraintWidth);
element.setConstraintHeight(originalConstraintHeight);
}
}
@Nonnull
public Color getTextSelectionColor() {
return textSelectionColor;
}
@Override
public void onEvent(final NiftyLocaleChangedEvent event) {
setText(originalTextBeforeSpecialValues);
}
}