All Downloads are FREE. Search and download functionalities are using the official Maven repository.

com.dua3.utility.ui.Graphics Maven / Gradle / Ivy

There is a newer version: 15.0.2
Show newest version
package com.dua3.utility.ui;

import com.dua3.utility.data.Color;
import com.dua3.utility.math.geometry.AffineTransformation2f;
import com.dua3.utility.math.geometry.Rectangle2f;
import com.dua3.utility.math.geometry.Vector2f;
import com.dua3.utility.text.Alignment;
import com.dua3.utility.text.Font;
import com.dua3.utility.text.FontUtil;
import com.dua3.utility.text.RichText;
import com.dua3.utility.text.Run;
import com.dua3.utility.text.TextUtil;
import com.dua3.utility.text.VerticalAlignment;

import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import java.util.NoSuchElementException;
import java.util.function.Function;

/**
 * A generic interface defining drawing commands.
 * 

* The Graphics interface provides an abstraction from the underlying rendering toolkit. */ public interface Graphics extends AutoCloseable { /** * Retrieves the FontUtil compatible with this Graphics object. * * @return the FontUtil object */ FontUtil getFontUtil(); /** * Retrieves the default font for the graphics object. * * @return the default font */ Font getDefaultFont(); /** * Get bounds. * * @return the bounding rectangle */ Rectangle2f getBounds(); /** * Get text dimensions using the current font. * * @param text the text * @return the text dimensions */ Rectangle2f getTextDimension(CharSequence text); /** * Stroke rectangle. * @param r the recatngle */ default void strokeRect(Rectangle2f r) { strokeRect(r.xMin(), r.yMin(), r.width(), r.height()); } /** * Draws the outline of a rectangle with the specified dimensions and coordinates. * * @param x the x-coordinate of the top-left corner of the rectangle * @param y the y-coordinate of the top-left corner of the rectangle * @param w the width of the rectangle * @param h the height of the rectangle */ void strokeRect(float x, float y, float w, float h); /** * Fill rectangle. * * @param r the rectangle */ default void fillRect(Rectangle2f r) { fillRect(r.xMin(), r.yMin(), r.width(), r.height()); } /** * Fills a rectangle with the specified dimensions and coordinates. * * @param x the x-coordinate of the top-left corner of the rectangle * @param y the y-coordinate of the top-left corner of the rectangle * @param w the width of the rectangle * @param h the height of the rectangle */ void fillRect(float x, float y, float w, float h); /** * Draws a line between two specified points. * * @param a the starting point of the line * @param b the ending point of the line */ default void strokeLine(Vector2f a, Vector2f b) { strokeLine(a.x(), a.y(), b.x(), b.y()); } /** * Draws a line on the graphics context from the specified starting point (x1, y1) to * the specified ending point (x2, y2). * * @param x1 the x-coordinate of the starting point * @param y1 the y-coordinate of the starting point * @param x2 the x-coordinate of the ending point * @param y2 the y-coordinate of the ending point */ void strokeLine(float x1, float y1, float x2, float y2); /** * Sets the stroke for drawing shapes. The stroke determines the color and width of the lines used to outline shapes. * * @param c the color of the stroke * @param width the width of the stroke, in pixels */ void setStroke(Color c, float width); /** * Sets the fill color for drawing operations. * * @param c the color to set as the fill color */ void setFill(Color c); /** * Sets the transformation for the graphics context. * * @param t the affine transformation to set */ void setTransformation(AffineTransformation2f t); /** * Sets the font used for text rendering. * * @param f the font to set */ void setFont(Font f); /** * Draws the specified text at the given coordinates. * * @param text the text to be drawn * @param x the x-coordinate of the starting point * @param y the y-coordinate of the starting point */ void drawText(CharSequence text, float x, float y); /** * Calculates the bounding rectangle of the graphics in the local coordinate space. * *

The method first retrieves the inverse of the current transformation using {@link #getTransformation()} and * throws an exception if the transformation is not present. Then, it calculates the bounds of the graphics using * the {@link #getBounds()} method. Finally, it transforms the minimum and maximum coordinates of the bounds using * the inverse transformation to obtain the bounds in the local coordinate space.

* * @return the bounding rectangle in the local coordinate space * @throws NoSuchElementException if the inverse of the current transformation is not present */ default Rectangle2f getBoundsInLocal() { AffineTransformation2f ti = getTransformation().inverse().orElseThrow(); Rectangle2f bounds = getBounds(); return Rectangle2f.withCorners( ti.transform(bounds.min()), ti.transform(bounds.max()) ); } enum HAnchor { LEFT, RIGHT, CENTER } enum VAnchor { TOP, BOTTOM, BASELINE, MIDDLE } /** * Draw text at the specified coordinates with the given horizontal and vertical anchor. * * @param text the text to be drawn * @param x the x-coordinate of the starting position for the text * @param y the y-coordinate of the starting position for the text * @param hAnchor the horizontal anchor for the text position (LEFT, RIGHT, or CENTER) * @param vAnchor the vertical anchor for the text position (TOP, BOTTOM, BASELINE, or MIDDLE) */ default void drawText(CharSequence text, float x, float y, HAnchor hAnchor, VAnchor vAnchor) { Rectangle2f r = getTextDimension(text); float tx, ty; tx = switch (hAnchor) { case LEFT -> x; case RIGHT -> x - r.width(); case CENTER -> x - r.width() / 2; }; ty = switch (vAnchor) { case TOP -> y - r.yMin(); case BOTTOM -> y - r.yMax(); case BASELINE -> y + r.height() - r.y(); case MIDDLE -> y + r.height() / 2 - r.yMax(); }; drawText(text, tx, ty); } /** * Retrieves the affine transformation of the graphics object. * * @return the affine transformation of the graphics object */ AffineTransformation2f getTransformation(); /** * Renders the given text within the specified bounding rectangle using the provided font, * alignment, and wrapping settings. * * @param r the bounding rectangle to render the text into * @param text the text to be rendered * @param font the font to use for rendering the text * @param hAlign the horizontal alignment of the text within the bounding rectangle * @param vAlign the vertical alignment of the text within the bounding rectangle * @param wrapping determines if text wrapping should be applied */ default void renderText(Rectangle2f r, RichText text, Font font, Alignment hAlign, VerticalAlignment vAlign, boolean wrapping) { FragmentedText fragments = generateFragments(text, r, font, hAlign, vAlign, wrapping); renderFragments(r, hAlign, vAlign, fragments.textWidth(), fragments.textHeight(), fragments.baseLine(), fragments.fragmentLines()); } /** * A text fragment that can't be split (i.e., contains no whitespace) and has a uniform font so that it can be * drawn in a single operation. * * @param x the x-position * @param y the y-position * @param w the width * @param h the height * @param baseLine the basline value (of the line the fragment belongs to * @param font the font * @param text the text */ record Fragment (float x, float y, float w, float h, float baseLine, Font font, CharSequence text) {} /** * Represents a fragmented text that can be rendered within a specified bounding rectangle. *

* The FragmentedText class holds a list of lines, where each line is represented by a list of {@link Fragment} * objects. Fragments are text segments that cannot be split and have uniform font attributes. The text is split * based on whitespace characters and text decorations (font, text decoration). The class also stores the dimensions * and position of the text within the bounding rectangle. * * @param fragmentLines a list of lines, where each line is represented by a list of Fragment objects * @param textWidth the width of the rendered text * @param textHeight the height of the rendered text * @param baseLine the baseline value of the line the fragment belongs to */ record FragmentedText(List> fragmentLines, float textWidth, float textHeight, float baseLine) {} /** * Split text into fragments. * *

Split the text into fragments that are either whitespace or free of whitespace and have uniform * text attributes (font, text decoration). For each line, a list of such fragments is generated and added * to the list of fragment lines (see {@link FragmentedText#fragmentLines()}). * * @param text the text * @param r the bounding rectangle to render the text into * @param font the default font * @param hAlign the horizontal alignment * @param vAlign thee vertical alignment * @param wrap if wrapping should be applied ( * @return the fragmented text as a {@link FragmentedText} instance */ private FragmentedText generateFragments(RichText text, Rectangle2f r, Font font, Alignment hAlign, VerticalAlignment vAlign, boolean wrap) { float wrapWidth = wrap ? r.width() : Float.MAX_VALUE; Function trimLine = switch (hAlign) { case LEFT -> RichText::stripTrailing; case RIGHT -> RichText::stripLeading; case CENTER, JUSTIFY -> RichText::strip; }; // generate lists of chunks for each line FontUtil fontUtil = getFontUtil(); List> fragmentLines = new ArrayList<>(); float textWidth = 0f; float textHeight = 0f; float baseLine = 0f; for (RichText line: text.split("\n")) { line = trimLine.apply(line); List fragments = new ArrayList<>(); fragmentLines.add(fragments); float xAct = 0f; float lineHeight = 0f; float lineWidth = 0f; float lineBaseLine = 0f; boolean wrapAllowed = false; for (var run: splitLinePreservingWhitespace(line, wrap)) { Font f = font.deriveFont(run.getFontDef()); Rectangle2f tr = fontUtil.getTextDimension(run, f); if (wrapAllowed && xAct + tr.width() > wrapWidth) { if (!fragments.isEmpty() && TextUtil.isBlank(fragments.get(fragments.size() - 1).text())) { // remove trailing whitespace fragments.remove(fragments.size() - 1); } else if (TextUtil.isBlank(run)) { // skip leading whitespace after wrapped line continue; } fragments = new ArrayList<>(); fragmentLines.add(fragments); xAct = 0f; textHeight += lineHeight; fragments.add(new Fragment(xAct, textHeight, tr.width(), tr.height(), lineBaseLine, f, run)); xAct += tr.width(); lineWidth = tr.width(); lineHeight = tr.height(); wrapAllowed = false; lineBaseLine = -tr.yMin(); } else { wrapAllowed = wrap; fragments.add(new Fragment(xAct, textHeight, tr.width(), tr.height(), lineBaseLine, f, run)); xAct += tr.width(); lineWidth += tr.width(); lineHeight = Math.max(lineHeight, tr.height()); lineBaseLine = Math.max(lineBaseLine, -tr.yMin()); } } textWidth = Math.max(textWidth, lineWidth); textHeight += lineHeight; baseLine = lineBaseLine; } return new FragmentedText(fragmentLines, textWidth, textHeight, baseLine); } /** * Renders text fragments within the specified bounding rectangle. The text fragments are provided as a list of fragment lines. * Each fragment line contains a list of fragments that are either whitespace or text with uniform attributes (font, text decoration). * The method calculates the positioning of each fragment based on the alignment and distributes whitespace and remaining space accordingly. * The rendered text is drawn on the graphics context. * * @param cr the bounding rectangle within which the text fragments will be rendered * @param hAlign the horizontal alignment of the text within the bounding rectangle * @param vAlign the vertical alignment of the text within the bounding rectangle * @param textWidth the total width of the text fragments within a line * @param textHeight the total height of the text fragments within all lines * @param baseLine the baseline position of the text fragments * @param fragmentLines a list of fragment lines, where each line contains a list of fragments */ private void renderFragments(Rectangle2f cr, Alignment hAlign, VerticalAlignment vAlign, float textWidth, float textHeight, float baseLine, List> fragmentLines) { float y = switch (vAlign) { case TOP, DISTRIBUTED -> cr.yMin(); case MIDDLE -> cr.yCenter() - textHeight /2; case BOTTOM -> cr.yMax() - textHeight; }; float fillerHeight = vAlign == VerticalAlignment.DISTRIBUTED ? (cr.height()- textHeight)/Math.max(1, fragmentLines.size()-1) : 0f; record LineStatistics(float text, float whiteSpace, int nSpace) {} for (int i = 0; i < fragmentLines.size(); i++) { List fragments = fragmentLines.get(i); // determine the number and size of whitespace and text fragments LineStatistics fi = fragments.stream().map(fragment -> { boolean isWS = TextUtil.isBlank(fragment.text()); return new LineStatistics(isWS ? 0f : fragment.w(), isWS ? fragment.w() : 0f, isWS ? 1 : 0); }) .reduce((a, b) -> new LineStatistics(a.text + b.text, a.whiteSpace + b.whiteSpace, a.nSpace + b.nSpace)) .orElseGet(() -> new LineStatistics(0f, 0f, 1)); float spaceToDistribute = cr.width() - fi.text - fi.whiteSpace; float totalSpace = fi.whiteSpace + spaceToDistribute; // when justify aligning text, use left alignment for the last line boolean isLastLine = i == fragmentLines.size() - 1; Alignment effectiveHAlign = (hAlign == Alignment.JUSTIFY && isLastLine) ? Alignment.LEFT : hAlign; float x = cr.xMin(); for (Fragment fragment : fragments) { switch (effectiveHAlign) { case JUSTIFY -> { // distribute the remaining space by evenly expanding existind whitespace if (TextUtil.isBlank(fragment.text())) { x += fragment.w() * (totalSpace / fi.whiteSpace() - 1); } } case RIGHT -> { if (fragment.x() == 0f) { // push everything to the right x += spaceToDistribute; } } case CENTER -> { if (fragment.x() == 0f) { // push everything halfway right x += spaceToDistribute / 2f; } } case LEFT -> { /* nothing to do */} } setFont(fragment.font); drawText(fragment.text.toString(), x + fragment.x, y + fragment.y + baseLine); } y += fillerHeight; } } /** * Splits a RichText line into fragments, preserving whitespace if wrapping is enabled. * Each fragment is either whitespace or text with uniform attributes (font, text decoration). * For each line, a list of such fragments is generated and returned. * * @param line the RichText line to be split * @param wrapping a boolean value indicating whether wrapping should be applied * @return a List of Run fragments representing the split line */ private static List splitLinePreservingWhitespace(RichText line, boolean wrapping) { if (!wrapping) { return line.runs(); } return Arrays.stream(line.split("(?<=\\s)|(?=\\s)")) .flatMap(part -> part.runs().stream()) .toList(); } @Override void close(); }





© 2015 - 2025 Weber Informatics LLC | Privacy Policy