Implementing Breadcrumbs in JavaFX

Posted on Mon 04 November 2013 in JavaFX

I've been struggling to implement a JavaFX breadcrumbs bar using a ToolBar with a few Button items in it. The breadcrumbs should have the shape of an arrow as in the example shown in Loop81's blog post.

The breadcrumbs bar in the Ensemble application uses the same approach. What I don't like about this implementation is the use of rather sophisticated CSS rules in combination with static images.

.breadcrumbs .item {
  -fx-background-color: null;
  -fx-background-radius: 0;
  -fx-background-insets: 0; 
  -fx-border-color: null;
  -fx-border-insets: 0;
  -fx-border-image-source: url("crumb.png");
  -fx-border-image-slice: 1 10 1 10 fill;
  -fx-border-image-width: 1 10 1 10;
  -fx-border-image-repeat: stretch;
  -fx-font-size: 18px;
}

.breadcrumbs .item:hover {
  -fx-border-image-source: url("crumb-hover.png");
}

.breadcrumbs .alone {
  -fx-border-image-width: 0 0 0 0;
  -fx-border-image-repeat: stretch;
  -fx-border-image-source: url("crumb-selected.png");
  -fx-border-color: transparent #6e737d transparent rgba(255,255,255,0.3) , transparent rgba(255,255,255,0.3) transparent transparent;
  -fx-border-insets: 0 0 1 0, 1;
}

...

As another approach, I tried to define an arrow-shaped SVG path (using the -fx-shape CSS rule) for the breadcrumb buttons, but I encountered scaling issues. The width of the shape must depend on the width of the text displayed in the button, which could not be achieved using CSS.

.breadcrumbs .item {
  ...
  -fx-shape: "M 0 0 L 100 0 L 130 15 L 100 30 L 0 30 z";
}

So I defined the shape in Java code and used it to set the button's clip. The width of the shape was computed dynamically based on the button's text plus two magic numbers for the correct scaling of the shape. This worked nicely, but came with the burden of those two magic numbers that worked well only for the particular font I used.

int textLength = button.getText.length();
// scaling and padding are magic numbers
int mainWidth = TEXT_SCALING * textLength + TEXT_PADDING;

// build the following shape
// --------
//
// / /
// --------
Path path = new Path();

// start in upper left corner
MoveTo e1 = new MoveTo(0, 0);

// draw upper horizontal line
HLineTo e2 = new HLineTo(ARROW_WIDTH + mainWidth);

// draw upper part of right arrow
LineTo e3 = new LineTo(ARROW_WIDTH + mainWidth + ARROW_WIDTH, ARROW_HEIGHT / 2.0);

// draw lower part of right arrow
LineTo e4 = new LineTo(ARROW_WIDTH + mainWidth, ARROW_HEIGHT);

// draw lower horizontal line
HLineTo e5 = new HLineTo(0);

// draw lower part of left arrow
LineTo e6 = new LineTo(ARROW_WIDTH, ARROW_HEIGHT / 2.0);

// close path
ClosePath e7 = new ClosePath();
path.getElements().addAll(e1, e2, e3, e4, e5, e6, e7);

// this is a dummy color to fill the shape, it won't be visible
path.setFill(Color.BLACK);

// set path as shape for button
button.setClip(path);

// make button width equal to width of shape
double buttonWidth = 2 * ARROW_WIDTH + mainWidth;
button.setMinWidth(buttonWidth);
button.setMaxWidth(buttonWidth);
button.setPrefWidth(buttonWidth);

In yet another attempt, I implemented a breadcrumbs bar in which every breadcrumb is represented by three buttons: one for the left-hand part of the arrow, one for the central part (showing the text), and one for the right-hand part. This allowed me to work with fixed-width shapes on the left and on the right plus a regular-shaped (rectangular) button in the middle. The obvious drawback was the management of all the extra buttons including the correct update of hover effects for the outer buttons when the mouse was over the inner button, and vice versa.

I don't think there a JavaFX library out there that provides the functionality I need, so is there anyone with experience in implementing a breadcrumbs bar? Any thoughts or recommendations? Are there solutions that do not come with any of the drawbacks mentioned above?

Any help will be much appreciated.

Update (2013-11-06):

Andy Till suggested an implementation that won my heart: bind the x property of the horizontal lines in the arrow shape to the button's width. Since the button's width gets updated to fit the text, the shape will always have the correct width. No need for magic numbers. Thanks, Andy!

// build the following shape
// --------
//
// / /
// --------
Path path = new Path();

// begin in the upper left corner
MoveTo e1 = new MoveTo(0, 0);

// draw a horizontal line that defines the width of the shape
HLineTo e2 = new HLineTo();
// bind the width of the shape to the width of the button
e2.xProperty().bind(button.widthProperty().subtract(ARROW_WIDTH));

// draw upper part of right arrow
LineTo e3 = new LineTo();
// the x endpoint of this line depends on the x property of line e2
e3.xProperty().bind(e2.xProperty().add(ARROW_WIDTH));
e3.setY(ARROW_HEIGHT / 2.0);

// draw lower part of right arrow
LineTo e4 = new LineTo();
// the x endpoint of this line depends on the x property of line e2
e4.xProperty().bind(e2.xProperty());
e4.setY(ARROW_HEIGHT);

// draw lower horizontal line
HLineTo e5 = new HLineTo(0);

// draw lower part of left arrow
LineTo e6 = new LineTo(ARROW_WIDTH, ARROW_HEIGHT / 2.0);

// close path
ClosePath e7 = new ClosePath();

path.getElements().addAll(e1, e2, e3, e4, e5, e6, e7);
// this is a dummy color to fill the shape, it won't be visible
path.setFill(Color.BLACK);

// set path as button shape
button.setClip(path);

And this is what my breadcrumbs bar looks like now:

breadcrumbs