Skip to content

Making a Resizeable UI

p0nce edited this page Jun 9, 2022 · 19 revisions

The CliptIt and Distort plug-in examples are the reference examples to copy in order to make your UI resizeable.
However, it is strongly recommended to read this guide though, as some of the details are important.
Here is a step by step guide to make your plug-in UI resizeable.


STEP 1. Create your UI with a SizeConstraints

The constructor of FlatBackgroundGUI and PBRBackgroundGUI, instead of just a fixed pixel size, can take a SizeConstraints that describes valid user size (and the initial size).

Several options exist to get one SizeConstraints.

  • makeSizeConstraintsDiscrete (recommended)

    This allows several discrete size, preserving aspect-ratio. Width and height of the plugin are scaled by the proposed ratio.

    static immutable float[7] ratios = [0.5f, 0.75f, 1.0f, 1.25f, 1.5f, 1.75f, 2.0f];
    super( makeSizeConstraintsDiscrete(640, 480, ratios) );
  • makeSizeConstraintsDiscreteXY (recommended)

    This allows separate discrete ratios for horizontal and vertical size. Width and height of the plugin are scaled by the two selected ratios. Not preserving aspect-ratio.

    static immutable float[7] ratiosX = [0.5f, 0.75f, 1.0f, 1.25f, 1.5f, 1.75f, 2.0f];
    static immutable float[1] ratiosY = [1.0f];
    super( makeSizeConstraintsDiscreteXY(640, 480, ratiosX, ratiosY) ); // plugin can only be resized alongside X dimension, with 7 possible widths.
  • makeSizeConstraintsFixed

    This doesn't allow resizing. It is the default that happens when no SizeConstraints is provided.

    super( makeSizeConstraintsFixed(640, 480) );  // equivalent to the former `super(640, 480);`
  • makeSizeConstraintsContinuous

    This allows a continuous range of scale factors, preserving aspect-ratio.

    super( makeSizeConstraintsContinuous(640, 480, 0.5f, 2.0f) );
  • makeSizeConstraintsBounds

    This allows a range of possible width, and a range of possible height, not preserving aspect-ratio.

    int defaultWidth  = 640;
    int minWidth      = defaultWidth / 2;
    int maxWidth      = defaultWidth * 2;
    int defaultHeight = 640;
    int minHeight     = defaultHeight / 2;
    int maxHeight     = defaultHeight * 2;
    super( makeSizeConstraintsBounds(minWidth, maxWidth, minHeight, maxHeight, defaultWidth, defaultHeight) );

    This is a better fit for plugins that can adapt their content to different aspect ratios.


STEP 2 - Use the reflow() method

What is reflow()?

The UIElement.reflow() method is the perfect place to set the .position of children widgets.
This will in turn call their reflow() method).

Key fact: reflow() should be relatively quick to avoid visual artifacts.

Inside reflow() or onDrawRaw/onDrawPBR, you can access the widget rectangle with the position getter.

Example

This is an example of a reflow() method at top-level:

override void reflow()
{
    // necessary for PBRBackgroundGUI and FlatBackgroundGUI
    super.reflow(); 

    // Get width and height of widget
    int W = position.width;
    int H = position.height;    
    
    // Get a scale factor for how the UI is resized. Note that this example is for fixed aspect-ratio.
    float S = W / cast(float)(context.getDefaultUIWidth());             

    // this position a widget, scaled, with rounded integer coordinates
    _myWidget.position = rectangle(517, 176, 46, 46).scaleByFactor(S); 

    // whenever there is a property expressed in pixels, it has to be set in `reflow()`
    _myWidget.fontSizeInPixels = 16 * S;

    // save S factor for later use (eg: line width in onDrawRaw)
    _S = S; 
}    

Note: reflow() is guaranteed to be called before drawing, at at each time position/size of a widget changes.


STEP 3 - Add a resizer corner widget

  1. Put in your UI member declaration:
private:
    UIWindowResizer _resizerCorner;
  1. Put in your UI constructor:
addChild(_resizerCorner= mallocNew!UIWindowResizer(context()));
  1. Put in your UI reflow():
int W = position.width;
int H = position.height;
_resizerCorner.position = rectangle(W-30, H-30, 30, 30);

Without this corner resizer, the plugin will only be resizeable in VST3. You can also make your own resizer widget.

To use UIWindowResizer, you will need dplug:flat-widgets as dependency.


STEP 4 - Upscale legacy graphics, resize at runtime (IMPORTANT TO READ)

Manual work

Obviously, as FlatBackgroundGUI and PBRBackgroundGUI now scales images to the selected user size, you will have some work to do to upscale existing the graphics to a larger size, in order to avoid blurriness. Using the largest size when visualizing may help.

For a brand new UI:

  • Design the UI at the largest size.
  • JPEG should probably be saved in 4:2:0 to save binary size.

For legacy graphics:

  • waifu2x can be used to upscale the more photographics parts of the UI: http://waifu2x.udp.jp/
  • Burn-in text usually has to be redone at a larger scale, or turned into a programmatic label.
  • Logos, filmstrips controls... must be upscaled too.

At runtime

At runtime, these resource images are now downscaled by widgets.

This can be done in two different ways:

  • If the image is small (like a logo), you can use the global resizer in reflow().
override void reflow()
{
    _imageResized.size(position.width, position.height);
    ImageResizer* resizer = context.globalImageResizer;    // only use this in reflow()
    resizer.resizeImageGeneric(_imageVanilla.toRef(), _imageResized.toRef());
}
  • If the image is large, then it's not recommended to resize it in reflow(). As reflow() should be fast to execute, it can be better to delay the resize to first-draw.

    • You can use a temporary ImageResizer in onDrawXXX (see FlatBackgroundGUI for reference)
    • or eventually resize only the parts necessary (see UIFilmstripKnob for reference).

    You cannot use the global UI resizer outside of reflow(), as onDrawXXX methods are called concurrently when possible.


STEP 5 - Avoid this subtle scaleByFactor trap.

Warning: watch out for this subtle trap with dirtyRect and scaled rectangles.

When you compute a sub-rectangle position with scaleByFactor, with the widget positionned itself with scaleByFactor, it's easy to pass to setDirty a rect that is outside _position. Clip the dirty rect to the width and height of the widget.