ANote.java

/* ========================================================================
 * PlantUML : a free UML diagram generator
 * ========================================================================
 *
 * (C) Copyright 2009-2024, 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:  Arnaud Roques
 *
 *
 */
package net.sourceforge.plantuml.asciiverse;

// The folded-corner ("dog-ear") note box, as an AsciiBlock: created by
// InfinitePlan.createNoteBox() and drawn later via asciiDraw(plan), against
// whatever InfinitePlan the caller has positioned at the box's top-left
// corner. Idea copied, not code, from the legacy
// asciiart.UmlCharAreaImpl.drawNoteSimple()/drawNoteSimpleUnicode() (that
// package is slated for removal): ASCII cuts a "!."/"|_\"
// dog-ear into the top-right corner instead of a plain corner character;
// Unicode draws a double-line box (deliberately different from the
// single-line participant box) plus a shaded block simulating the fold.
public final class ANote implements AsciiBlock {

	private final int width;
	private final int height;
	private final AsciiBlock text;

	// Size is derived from the content alone, once, at construction time —
	// never passed in and never recomputed elsewhere (the exact bug we hit
	// once already: a caller-side copy of this formula that forgot the `+2`).
	// `width = max(5, textWidth + 2)`, `height =
	// max(3, textHeight + 2)`: the `+2` on each axis is the border, the floors
	// are what the ASCII dog-ear (5-column minimum) and the 3-row border (top
	// + content + bottom) need. Notably this doesn't need to know
	// ASCII-vs-Unicode at all: both variants share the same width/height,
	// only the border *characters* differ (asciiDrawAscii()/asciiDrawUnicode()
	// below) — which is also why isUnicode isn't a constructor argument
	// either (see asciiDraw() below).
	public ANote(AsciiBlock text) {
		this.text = text;
		final ADimension2D textDim = text.asciiDimension();
		this.width = Math.max(5, textDim.getWidth() + 2);
		this.height = Math.max(3, textDim.getHeight() + 2);
	}

	@Override
	public ADimension2D asciiDimension() {
		return new ADimension2D(width, height);
	}

	@Override
	public void asciiDraw(InfinitePlan plan) {
		// isUnicode isn't stored on the object: like every other draw decision
		// (column, row), it's read off whatever `plan` this block is
		// asked to draw against, rather than fixed at construction. A caller
		// that only needs the box's size (e.g. CommunicationTileNoteRight/Left's
		// asciiDimension()) can build an ANote with nothing but the
		// text and never touch a plan at all.
		if (plan.isUnicode())
			asciiDrawUnicode(plan);
		else
			asciiDrawAscii(plan);

		// The text sits one column/row inside the border on every side (top
		// border row + left border column), exactly where the old
		// caller-side `plan.move(left + 1, 1).drawString(noteText)` used to
		// place it — now delegated to `text` itself, which may draw several
		// rows (see Display.asciiDraw()) instead of a single flattened line.
		text.asciiDraw(plan.move(1, 1));
	}

	private void asciiDrawAscii(InfinitePlan plan) {
		if (width > 2)
			plan.drawHLine(1, width - 2, 0);
		if (height > 2 && width > 2)
			plan.drawHLine(1, width - 2, height - 1);
		if (height > 2)
			plan.fillVLine('|', 1, height - 2, 0);
		if (height > 2 && width > 1)
			plan.fillVLine('|', 1, height - 2, width - 1);

		plan.drawChar(',');
		if (height > 1)
			plan.move(0, height - 1).drawChar('`');
		if (height > 1 && width > 1)
			plan.move(width - 1, height - 1).drawChar('\'');

		// The dog-ear cut, overwriting the top-right corner: needs at least 4
		// columns (left corner + 2-char fold + at least one border column).
		if (width >= 4) {
			plan.move(width - 3, 0).drawString("!.");
			plan.move(width - 1, 0).drawChar(' ');
			if (height > 1)
				plan.move(width - 3, 1).drawString("|_\\");
		}
	}

	private void asciiDrawUnicode(InfinitePlan plan) {
		if (width > 2)
			plan.fillHLine('═', 1, width - 2, 0);
		if (height > 2 && width > 2)
			plan.fillHLine('═', 1, width - 2, height - 1);
		if (height > 2)
			plan.fillVLine('║', 1, height - 2, 0);
		if (height > 2 && width > 1)
			plan.fillVLine('║', 1, height - 2, width - 1);

		plan.drawChar('╔');
		if (width > 1)
			plan.move(width - 1, 0).drawChar('╗');
		if (height > 1)
			plan.move(0, height - 1).drawChar('╚');
		if (height > 1 && width > 1)
			plan.move(width - 1, height - 1).drawChar('╝');

		// Shaded block simulating the folded corner, one row below the top
		// border, one column left of the right border.
		if (width >= 2 && height >= 2)
			plan.move(width - 2, 1).drawChar('░');
	}

}