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.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.AbstractTextBlock;
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;

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;

	public PacketBlock(int width, int height, String label, ISkinParam skinParam) {
		this.width = width;
		this.height = height;
		this.label = label;
		this.skinParam = skinParam;
	}

	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;
	}

	public void openLeft(boolean leftOpen) {
		this.leftOpen = leftOpen;
	}

	public void openRight(boolean rightOpen) {
		this.rightOpen = rightOpen;
	}

	public int getWidth() {
		return width;
	}

	public int getHeight() {
		return height;
	}

	public void setHeight(int height) {
		this.height = height;
	}

	public double getDrawWidth(double scale) {
		return width * scale;
	}

	public double getDrawHeight(double scale) {
		return height * scale;
	}

	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) {
		// vertical(top and bottom) margin for displaying the label content
		final double vMargin = 10D;
		final double reqWidth = getDrawWidth(bitWidth);
		final Fashion fashion = getFashion();
		final TextBlock label = getLabelTextBlockAbbr(stringBounder, bitWidth);
		// mergeTB would add label height and margins to the shape so here this offset makes sure we actually get drawHeight
		double heightPreOffset = Math.max(0D, getDrawHeight(bitHeight) - label.calculateDimension(stringBounder).getHeight() - vMargin - vMargin);
		final TextBlock stereo = TextBlockUtils.empty(reqWidth, heightPreOffset);

		// Basically it's URectangle#asSmall, but without horizontal margin
		return new AbstractTextBlock() {
			@Override
			public XDimension2D calculateDimension(StringBounder stringBounder) {
				final XDimension2D dimLabel = label.calculateDimension(stringBounder);
				final XDimension2D dimStereo = stereo.calculateDimension(stringBounder);
				XDimension2D dim = dimStereo.mergeTB(dimLabel);
				return new XDimension2D(dim.getWidth(), dim.getHeight() + vMargin + vMargin);
			}

			@Override
			public void drawU(UGraphic ug) {
				final XDimension2D dim = calculateDimension(ug.getStringBounder());
				ug = UGraphicStencil.create(ug, dim);
				ug = fashion.apply(ug);
				// URectangle#drawRect
				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(stereo, label, HorizontalAlignment.CENTER);
				tb.drawU(ug.apply(new UTranslate(0D, vMargin)));
			}
		};
	}
}