Creating a HUD style button

January 3, 2009

hud_button

Creating a HUD style button UI delegate is relatively straight forward. We’ll need to provide a gradient, the appropriate insets, the font color, and the right amount of “roundedness” – all easy.

The trickiest part of the HUD button is it’s ever so slight drop shadow. The shadow is drawn directly beneath the button, and is just two pixels tall. I chose to take the easy way out and simulate the shadow. I did this by drawing the button outline twice directly below the button at buttonHeight+1 and buttonHeight+2. The closer outline uses a darker color, while the more offset outline uses a lighter color – more distance means a more diffuse shadow.

You’ll find the code below. Note that for this button to look “correct”, it must be added to a HUD style window, which I talked about here. The button is quite transparent, so it ends up looking a lot like what it’s added to.

public class HudButtonUI extends BasicButtonUI {

    /* Font constants. */
    public static final float FONT_SIZE = 11.0f;
    public static final Color FONT_COLOR = Color.WHITE;

    /* Color constants. */
    private static final Color TOP_COLOR = new Color(170,170,170,50);
    private static final Color BOTTOM_COLOR = new Color(17,17,17,50);
    private static final Color TOP_PRESSED_COLOR = new Color(249,249,249,153);
    private static final Color BOTTOM_PRESSED_COLOR = new Color(176,176,176,153);
    private static final Color LIGHT_SHADOW_COLOR = new Color(0,0,0,145);
    private static final Color DARK_SHADOW_COLOR = new Color(0,0,0,50);

    /* Border constants. */
    private static final Color BORDER_COLOR = new Color(0xc5c8cf);
    private static final int BORDER_WIDTH = 1;

    /* Margin constants. */
    private static final int TOP_AND_BOTTOM_MARGIN = 2;
    private static final int LEFT_AND_RIGHT_MARGIN = 16;

    private final Roundedness fRoundedness;

    /**
     * Creates a HUD style {@link javax.swing.plaf.ButtonUI} with full rounded edges.
     */
    public HudButtonUI() {
        fRoundedness = Roundedness.ROUNDED_BUTTON;
    }

    @Override
    protected void installDefaults(AbstractButton b) {
        super.installDefaults(b);

        // TODO save original values.

        b.setFont(getHudFont());
        b.setForeground(FONT_COLOR);
        b.setOpaque(false);
        b.setHorizontalTextPosition(AbstractButton.CENTER);
        // add space for the drop shadow underneath the button.
        int bottomMargin = TOP_AND_BOTTOM_MARGIN + getHudControlShadowSize();
        b.setBorder(BorderFactory.createEmptyBorder(TOP_AND_BOTTOM_MARGIN, 
                LEFT_AND_RIGHT_MARGIN,bottomMargin, LEFT_AND_RIGHT_MARGIN));
    }

    @Override
    public void paint(Graphics g, JComponent c) {
        AbstractButton button = (AbstractButton) c;
        Graphics2D graphics = (Graphics2D) g.create();

        // paint the HUD button border and background in the visual space that the
        // button should take up (the area not including the drop shadow). note
        // that the paint method will also paint the shadow "below" button.
        int buttonHeight = button.getHeight() - getHudControlShadowSize();
        paintHudControlBackground(graphics, button, button.getWidth(),
                buttonHeight, fRoundedness);

        graphics.dispose();

        // now that the background is painted, call the super.paint.
        super.paint(g, c);
    }

    /**
     * Paints a HUD style button background onto the given {@link Graphics2D} context.
     * The background will be painted from 0,0 to width/height.
     * @param graphics the graphics context to paint onto.
     * @param button the button being painted.
     * @param width the width of the area to paint.
     * @param height the height of the area to paint.
     * @param roundedness the roundedness to use when painting the background.
     */
    public static void paintHudControlBackground(
            Graphics2D graphics, AbstractButton button, int width, int height,
            Roundedness roundedness) {
        graphics.setRenderingHint(
                RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON);

        // TODO replace with real drop shadow painting. see Romain Guy's article for
        // TODO more info on real drop shadows:
        // TODO   http://www.jroller.com/gfx/entry/fast_or_good_drop_shadows

        // paint a light shadow line further away from the button.
        graphics.setColor(LIGHT_SHADOW_COLOR);
        int arcDiameter = roundedness.getRoundedDiameter(height);
        graphics.drawRoundRect(0,0,width-1,height,arcDiameter,arcDiameter);

        // paint a dark shadow line closer to the button.
        graphics.setColor(DARK_SHADOW_COLOR);
        int smallerShadowArcDiameter = height-1;
        graphics.drawRoundRect(0,0,width-1,height+1,smallerShadowArcDiameter,
                smallerShadowArcDiameter);

        // fill the button with the gradient paint.
        graphics.setPaint(createButtonPaint(button, BORDER_WIDTH));
        graphics.fillRoundRect(0,1,width,height-1,arcDiameter,arcDiameter);

        // draw the border around the button.
        graphics.setColor(BORDER_COLOR);
        graphics.drawRoundRect(0,0,width-1,height-1,arcDiameter,arcDiameter);
    }

    /**
     * Creates a HUD style gradient paint for the given button offset from the top
     * and bottom of the button by the given line border size.
     */
    private static Paint createButtonPaint(AbstractButton button,
                                           int lineBorderWidth) {
        boolean isPressed = button.getModel().isPressed();
        Color topColor = isPressed ? TOP_PRESSED_COLOR : TOP_COLOR;
        Color bottomColor = isPressed ? BOTTOM_PRESSED_COLOR : BOTTOM_COLOR;
        int bottomY = button.getHeight()-lineBorderWidth*2;
        return new GradientPaint(0,lineBorderWidth,topColor,0,bottomY,bottomColor);
    }

    /**
     * Gets the number of pixels that a HUD style widget's shadow takes up. HUD
     * button's have a shadow directly below them, that is, there is no top, left
     * or right component to the shadow.
     * @return the number of pixels that a HUD style widget's shadow takes up.
     */
    private static int getHudControlShadowSize() {
        // this is hardcoded at two pixels for now, but ideally it would be
        // calculated.
        return 2;
    }

    /**
     * Gets the font used by HUD style widgets.
     * @return the font used by HUD style widgets.
     */
    private static Font getHudFont() {
        return UIManager.getFont("Button.font").deriveFont(Font.BOLD, FONT_SIZE);
    }

    /**
     * An enumeration representing the roundness styles of HUD buttons. Using this 
     * enumeration will make it easier to transition this code to support more 
     * HUD controls, like check boxes and combo buttons.
     */
    public enum Roundedness {
        /**
         * A roundedness of 95%, equates to almost a half-circle as the button
         * edge shape.
         */
        ROUNDED_BUTTON(.95);

        private final double fRoundedPercentage;

        private Roundedness(double roundedPercentage) {
            fRoundedPercentage = roundedPercentage;
        }

        private int getRoundedDiameter(int controlHeight) {
            int roundedDiameter = (int) (controlHeight * fRoundedPercentage);
            // force the rounded diameter value to be even - odd values look lumpy.
            int makeItEven = roundedDiameter % 2;
            return roundedDiameter - makeItEven;
        }
    }
}
Advertisements

14 Responses to “Creating a HUD style button”

  1. Peter Says:

    Great Stuff!

    And i already know where I need that buttons =)
    Hope they look right with JToggleButton.

    Way to go, Ken!

    -Peter

  2. Ken Says:

    Hi Peter,

    I should have mentioned that Mac Widgets for Java 0.9.4 will contain the following UI delegates:

    HudButtonUI
    HudCheckBoxUI
    HudComboBoxUI
    HudLabelUI

    The actual production painting mechanism relies heavily on the reusable utility class HudPaintingUtils.

    Enjoy!
    -Ken

  3. Caligula Says:

    “[…] is *its* ever-so-slight drop shadow.”

    Also, BGHUDAppKit (http://www.binarymethod.com/content/bghudappkit.php)

  4. Mohamed Mansour Says:

    Very nice :)

  5. Harald K. Says:

    As always, it looks good. :-)

    What would be nice, was if the UI delegate was automatically installed on the button, when the button is added to a HUD style window.

    Another thing that would be nice is a HudTextFieldUI for text fields on HUDs.

  6. Ken Says:

    Caligula,

    These controls are nice, but they are Cocoa based. Another Cocoa based solution I’ve stumbled across that pays close attention to detail is Brandon Walkin’s BWToolkit.

    -Ken

  7. Ken Says:

    Harald,

    I like the way you think! It would indeed be nice if these HUD style UI delegates installed themselves auto-magically when you added them to a HUD style window – I’ll look into this.

    Nimubs supports this sort of component-tree skinning out-of-the box, which will be very useful, though your base L&F will have to be Nimbus.

    -Ken
    P.S. I’ve added your request for a JTextFiled HUD UI delegate to the corresponding Mac Widgets for Java issue here.

  8. rbs Says:

    Jeremy,

    I’m having a problem with this in that the button always looks the size of a regular Mac button, with a fair amount of empty space above and below the text. It definitely looks much bigger than the sample in the image at the top of your post.

    Also, have you looked at this from the context of being to respond to whether the programmer tries to apply one of the size properties that Apple added to Swing components a year ago?

    I have my own simple code for a HUD-looking JTextField, although it will be interesting to see what up with. But I’m looking forward to whatever other components you come up with.

  9. rbs Says:

    Sorry, brain spasm. I don’t know why I was saying “Jeremy”, Ken.

  10. Ken Says:

    Hi rbs,

    I’m not quite sure what you mean by “the button always looks the size of a regular Mac button, with a fair amount of empty space above and below the text”. This could be an issue with Java Update 1 vs Java Update 2 on the Mac (I authored this delegate with Update 1). I have seen issues with insets not being honored correctly in Update 2.

    Could you email me a screen shot?

    I hadn’t thought about the different size variants, but that is a good idea. I’ll keep it in mind.

    -Ken

  11. Peter Says:

    Hi Ken!

    I modified your code so that it works with JToggleButton too.

    add in header:
    private static final Color TOP_SELECTED_COLOR = new Color(200,200,200,153);
    private static final Color BOTTOM_SELECTED_COLOR = new Color(111,111,111,153);

    add/change in createButtonPaint:

    boolean isSelected = button.getModel().isSelected();
    Color topColor = isPressed ? TOP_PRESSED_COLOR : (isSelected ? TOP_SELECTED_COLOR : TOP_COLOR);
    Color bottomColor = isPressed ? BOTTOM_PRESSED_COLOR : (isSelected ? BOTTOM_SELECTED_COLOR : BOTTOM_COLOR);

    The conditional ifs are not pretty, and i don’t know if thats the “right” color (i just played around with the values) but it looks quite good.

    -Peter

  12. Ken Says:

    Cool…thanks Peter. I’ll look at incorporating your changes.


  13. […] you’ll probably notice a bit of redundant code from the post on creating a HUD style button. I’ve done this to keep the blog entries independent, but the code that will be incorporated […]


  14. […] for drawing the combo box itself. It does this by creating a simple HUD style button (see this post for the HudButtonUI code), with extra margin space on the right. Up/down arrows are then painted in […]


Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out / Change )

Twitter picture

You are commenting using your Twitter account. Log Out / Change )

Facebook photo

You are commenting using your Facebook account. Log Out / Change )

Google+ photo

You are commenting using your Google+ account. Log Out / Change )

Connecting to %s

%d bloggers like this: