PacketBlock.java
/* ========================================================================
* PlantUML : a free UML diagram generator
* ========================================================================
*
* (C) Copyright 2009-2026, Arnaud Roques
*
* Project Info: https://plantuml.com
*
* If you like this project or if you find it useful, you can support us at:
*
* https://plantuml.com/patreon (only 1$ per month!)
* https://plantuml.com/paypal
*
* This file is part of PlantUML.
*
* PlantUML is free software; you can redistribute it and/or modify it
* under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* PlantUML distributed in the hope that it will be useful, but
* WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
* or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public
* License for more details.
*
* You should have received a copy of the GNU General Public
* License along with this library; if not, write to the Free Software
* Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301,
* USA.
*
*
* Original Author: kolulu23
*
*
*/
package net.sourceforge.plantuml.packetdiag;
import net.sourceforge.plantuml.annotation.Fast;
import net.sourceforge.plantuml.klimt.Fashion;
import net.sourceforge.plantuml.klimt.LineBreakStrategy;
import net.sourceforge.plantuml.klimt.Shadowable;
import net.sourceforge.plantuml.klimt.UStroke;
import net.sourceforge.plantuml.klimt.UTranslate;
import net.sourceforge.plantuml.klimt.creole.CreoleMode;
import net.sourceforge.plantuml.klimt.creole.Display;
import net.sourceforge.plantuml.klimt.drawing.UGraphic;
import net.sourceforge.plantuml.klimt.drawing.UGraphicStencil;
import net.sourceforge.plantuml.klimt.font.StringBounder;
import net.sourceforge.plantuml.klimt.font.UFont;
import net.sourceforge.plantuml.klimt.geom.HorizontalAlignment;
import net.sourceforge.plantuml.klimt.geom.XDimension2D;
import net.sourceforge.plantuml.klimt.shape.TextBlock;
import net.sourceforge.plantuml.klimt.shape.TextBlockUtils;
import net.sourceforge.plantuml.klimt.shape.URectangle;
import net.sourceforge.plantuml.style.ISkinParam;
import net.sourceforge.plantuml.style.SName;
import net.sourceforge.plantuml.style.Style;
import net.sourceforge.plantuml.style.StyleSignatureBasic;
/**
* A single drawable field (block) in a {@code packetdiag} diagram.
* <p>
* A {@link PacketBlock} represents a rectangular region spanning a given number of bit columns
* ({@code width}) and a given vertical size ({@code height}), with an optional label rendered inside.
* The block can be marked as "open" on the left and/or right side to indicate that it is a continuation
* of the same logical field split across multiple rows.
* </p>
* <p>
* This is a low-level rendering model used by {@link PacketDiagram} when laying out {@code PacketItem}s
* into a row grid. Actual pixel dimensions are obtained by multiplying bit units by the current scale
* (bit width/height) and adding margins/padding during shape creation.
* </p>
*/
public class PacketBlock {
private static final UStroke openerStroke = new UStroke(5, 5, 1);
/**
* Desired block width unit, this is not the final drawing dimension as margins,
* paddings and scaling are not added
*/
private final int width;
/**
* Desired block height unit
*/
private int height;
/**
* Full label text
*/
private final String label;
private final ISkinParam skinParam;
private boolean leftOpen = false;
private boolean rightOpen = false;
/**
* Creates a closed block with the given bit-size and label.
*
* @param width
* block width in bit units (columns)
* @param height
* block height in bit units (rows)
* @param label
* label to display inside the block (may be {@code null} depending on caller conventions)
* @param skinParam
* skin parameters used to resolve styles, fonts and colors
*/
public PacketBlock(int width, int height, String label, ISkinParam skinParam) {
this.width = width;
this.height = height;
this.label = label;
this.skinParam = skinParam;
}
/**
* Creates a block with optional open edges to represent a field continued across rows.
*
* @param width
* block width in bit units (columns)
* @param height
* block height in bit units (rows)
* @param label
* label to display inside the block
* @param skinParam
* skin parameters used to resolve styles, fonts and colors
* @param leftOpen
* {@code true} if the left border is "open" (continuation from a previous row)
* @param rightOpen
* {@code true} if the right border is "open" (continues on the next row)
*/
public PacketBlock(int width, int height, String label, ISkinParam skinParam, boolean leftOpen, boolean rightOpen) {
this.width = width;
this.height = height;
this.label = label;
this.skinParam = skinParam;
this.leftOpen = leftOpen;
this.rightOpen = rightOpen;
}
/**
* Marks whether the left edge should be rendered as open (continuation indicator).
*
* @param leftOpen {@code true} to open the left edge, {@code false} to close it
*/
public void openLeft(boolean leftOpen) {
this.leftOpen = leftOpen;
}
/**
* Marks whether the right edge should be rendered as open (continuation indicator).
*
* @param rightOpen {@code true} to open the right edge, {@code false} to close it
*/
public void openRight(boolean rightOpen) {
this.rightOpen = rightOpen;
}
/**
* Returns the block width in bit units.
*
* @return width in bit units (columns)
*/
public int getWidth() {
return width;
}
/**
* Returns the block height in bit units.
*
* @return height in bit units (rows)
*/
public int getHeight() {
return height;
}
/**
* Sets the block height in bit units.
*
* @param height height in bit units (rows)
*/
public void setHeight(int height) {
this.height = height;
}
/**
* Returns the block width in pixels for a given horizontal bit scale.
*
* @param scale pixels per bit
* @return width in pixels
*/
public double getDrawWidth(double scale) {
return width * scale;
}
/**
* Returns the block height in pixels for a given vertical bit scale.
*
* @param scale pixels per bit
* @return height in pixels
*/
public double getDrawHeight(double scale) {
return height * scale;
}
/**
* Returns the label text displayed inside this block.
*
* @return the block label (as provided at construction time)
* (And may be empty depending on caller conventions)
*/
public String getLabel() {
return label;
}
Style getStyle() {
return StyleSignatureBasic.of(SName.root, SName.element, SName.packetdiagDiagram, SName.rectangle)
.getMergedStyle(skinParam.getCurrentStyleBuilder());
}
Fashion getFashion() {
return getStyle().getSymbolContext(skinParam.getIHtmlColorSet());
}
private TextBlock getLabelTextBlock(String label) {
return Display.getWithNewlines(skinParam.getPragma(), label).create8(
getStyle().getFontConfiguration(skinParam.getIHtmlColorSet()), HorizontalAlignment.CENTER, skinParam,
CreoleMode.SIMPLE_LINE, LineBreakStrategy.NONE);
}
private TextBlock getLabelTextBlockAbbr(StringBounder stringBounder, double bitWidth) {
UFont font = getStyle().getUFont();
final String pad = "...";
XDimension2D padDim = stringBounder.calculateDimension(font, pad);
XDimension2D labelDim = stringBounder.calculateDimension(font, label);
double reqWidth = getDrawWidth(bitWidth);
if (labelDim.getWidth() < reqWidth) {
return getLabelTextBlock(label);
}
// Shrink label char by char, this is technically wrong when there's multiple
// Unicode code points
String abbr = label;
for (int i = label.length(); i > 0; i--) {
if (labelDim.getWidth() + padDim.getWidth() < reqWidth) {
break;
}
abbr = label.substring(0, i) + pad;
labelDim = stringBounder.calculateDimension(font, abbr);
}
return getLabelTextBlock(abbr);
}
TextBlock getShapeTextBlock(StringBounder stringBounder, double bitWidth, double bitHeight) {
final double vMargin = 10D;
final double reqWidth = getDrawWidth(bitWidth);
final Fashion fashion = getFashion();
final TextBlock label = getLabelTextBlockAbbr(stringBounder, bitWidth);
final double labelHeight = label.calculateDimension(stringBounder).getHeight();
final double totalSpare = Math.max(0D, getDrawHeight(bitHeight) - labelHeight - vMargin - vMargin);
final double topSpare = totalSpare / 2.0;
final double bottomSpare = totalSpare - topSpare;
final TextBlock topSpacer = TextBlockUtils.empty(reqWidth, topSpare);
final TextBlock bottomSpacer = TextBlockUtils.empty(reqWidth, bottomSpare);
return new TextBlock() {
@Override
@Fast
public XDimension2D calculateDimension(StringBounder stringBounder) {
final double w = reqWidth;
final double h = topSpare + labelHeight + bottomSpare + vMargin + vMargin;
return new XDimension2D(w, h);
}
@Override
public void drawU(UGraphic ug) {
final XDimension2D dim = calculateDimension(ug.getStringBounder());
ug = UGraphicStencil.create(ug, dim);
ug = fashion.apply(ug);
final URectangle rect = URectangle.build(dim.getWidth(), dim.getHeight());
final Shadowable shape = rect.rounded(fashion.getRoundCorner());
shape.setDeltaShadow(fashion.getDeltaShadow());
ug.draw(shape);
final TextBlock tb = TextBlockUtils.mergeTB(topSpacer, label, HorizontalAlignment.CENTER);
final TextBlock full = TextBlockUtils.mergeTB(tb, bottomSpacer, HorizontalAlignment.CENTER);
full.drawU(ug.apply(new UTranslate(0D, vMargin)));
}
};
}
}