KrabCode / LazyGui

GUI library for Processing in which you only mention control elements in draw() at the places you need them.
https://krabcode.github.io/LazyGui/
MIT License
83 stars 4 forks source link
gui processing user-interface

LazyGui header

Table of Contents

LazyGui is a GUI library for Processing

Main ideas

Quality of life features

How do I start using this?

with PDE (the default Processing editor)

with other IDEs like IntelliJ IDEA

Get the latest jar file from releases and then import it into your project using your IDE as a standard java library just like you imported Processing.

Minimal code example

import com.krab.lazy.*;

LazyGui gui;

void setup(){
    size(800,800,P2D);
    gui = new LazyGui(this);
}

void draw(){
    background(gui.colorPicker("background").hex);
}

root and options look like this

The gui displays itself at the end of draw() and by default it shows a root folder that can't be closed with two built in folders

Control elements

Slider

a slider looks like this

// simplest getter for an infinite slider
float x = gui.slider("x");

// alternative getters that specify defaults and constraints
gui.slider("x", defaultFloat);
gui.slider("x", defaultFloat, minimumFloat, maximumFloat);

// setters
gui.sliderAdd("x", floatToAdd);
gui.sliderSet("x", floatToSet);

Plot

a plot looks like this

// simplest getter
PVector pos = gui.plotXY("position");

// alternative getters that specify defaults
gui.plotXY("position", defaultFloatXYZ);
gui.plotXY("position", defaultFloatX, defaultFloatY);
gui.plotXY("position", defaultPVector);

//setters
gui.plotSet("position", valueFloat);
gui.plotSet("position", valueFloatX, valueFloatY);
gui.plotSet("position", valuePVector);

Color picker

a color picker looks like this

// simplest getter
PickerColor myColor = gui.colorPicker("background");
// use it with .hex in place of color
background(myColor.hex);

// alternative getters that specify the default color
gui.colorPicker("background", color(36));
gui.colorPicker("background", grayNorm); // 'norm' meaning float in the range [0, 1]
gui.colorPicker("background", hueNorm, saturationNorm, brightnessNorm);
gui.colorPicker("background", hueNorm, saturationNorm, brightnessNorm, alphaNorm);

// setters
gui.colorPickerSet("background", color(36));
gui.colorPickerHueAdd("background", hueToAdd);

Gradient picker

gradient pickers look like this

// simple getter
PGraphics bgGradient = gui.gradient("background gradient");
image(bgGradient, 0, 0);

// alternative getter that specifies the default colors
gui.gradient("name", new int[]{color(255,0,150), color(0,150,0), color(0,100,150)});

// alternative getter which allows you to specify default colors and positions
// it uses varargs so you can use two or more gui.colorPoint() parameters
gui.gradient("name",
  gui.colorPoint(color(255, 0, 0), 0f),
  gui.colorPoint(color(0, 255, 0), 0.5f),
  gui.colorPoint(color(0, 0, 255), 1f)
);

// special getter for a color inside the gradient at a position in range [0, 1]
// faster than texture.get(x, y) thanks to a color look up table
PickerColor myColor = gui.gradientColorAt("name", positionNorm);

Button

a button looks like this

// getter that is only true once after being clicked and then switches to false 
boolean clear = gui.button("clear");
if(clear){
    background(0.1);
    println("background cleared");
}

Toggle

a toggle looks like this

// simple getter
boolean isToggledOn = gui.toggle("spam every frame")
if(isToggledOn){
    println("I'm trapped in a string factory");
}

// alternative getter that specifies a default
gui.toggle("spam every frame", booleanDefault)

// setter
gui.toggleSet("spam every frame", booleanValue)

Text input

text input looks like this

// simple getter
String userInput = gui.text("text header");

// getter that specifies a default content
gui.text("text header", "this default text can be edited");
gui.text("", "this will rename its parent folder"); 

// one time setter that also blocks any interaction when called every frame
gui.textSet("text header", "content")
Mouse Hotkey Action under mouse
Enter insert new line
Delete delete entire string
Backspace delete last character

Radio

radio looks like this

// simplest getter
String mode = gui.radio("mode", new String[]{"square", "circle"});
if (mode.equals("square")) {
    rect(175, 175, 50, 50);
} else {
    ellipse(200, 200, 50, 50);
}

// getter that specifies a default
gui.radio("mode", stringArray, defaultOption);

// setter that changes the currently selected option
gui.radioSet("mode", "square");

// setter that specifies new options for an existing radio
gui.radioSetOptions("mode", new String[]{"square", "circle", "triangle"});

Hotkeys

Global hotkey Action
H Hide GUI / Show GUI
D Close windows
I Save screenshot
CTRL + Z Undo
CTRL + Y Redo
CTRL + S New save
Mouse hotkey Action on element under mouse
Right click Close window
R Reset value to default
CTRL + C Copy value or folder
CTRL + V Paste to value or folder

Mouse interaction

Interacting with your sketch using the mouse can be very useful, but the GUI can get in the way, usually you don't want the sketch to react when you're dragging a slider in the GUI.

Unfortunately the GUI has no way to block the sketch from receiving the mouse event, but it can tell you whether the mouse has interacted with the GUI thanks to the isMouseOutsideGui() method.

void mousePressed(){
    if(gui.isMouseOutsideGui()){
        // do something at the mouse
    }
}

see: isMouseOutsideGui(), isMouseOverGui(), MouseDrawing

Drawing the GUI manually

The GUI draws itself at the end of draw() by default, but you can override this by calling gui.draw() before that happens. The GUI will never draw itself more than once per frame, so the automatic execution is skipped when this is called manually.

This can be useful in cases like including the GUI in a recording or using the PeasyCam library where you probably want to display the GUI between cam.beginHUD() and cam.endHUD() to separate the GUI overlay from the camera controlled 3D scene.

see: draw(), PeasyCamExample

Saving and loading values

The GUI can save its current values to disk in a json file. It can also load these values to overwrite the current GUI state. You can control this from the saves folder under the root window of the GUI. Any new, renamed and deleted save files will be detected by this window at runtime.

save

Create save

Paths and folders

The path is the first string parameter to every control element function, and it must be unique. It exists only in memory to inform the GUI - it's not a directory structure in any file storage. The forward slash / is a reserved character used to make folders, but it can be escaped with \\ like this: \\/ which won't separate folders.

Creating a folder with the forward slash

wave folder example

float frq = gui.slider("wave/frequency");
float amp = gui.slider("wave/amplitude");

Escaping the forward slash

Escaped forward slash example

boolean state = gui.toggle("off\\/on");

Global path prefix stack

Repeating the whole path in every control element call can get tiresome, especially with multiple nested folders. Which is why there's a helpful path stack that you can interact with using pushFolder() and popFolder().

Just like using pushMatrix() and popMatrix() in Processing, you can change your "current directory" by pushing a new folder name to a stack with gui.pushFolder("folder name") and have every control element called after that be placed into that folder automatically as if the contents of the whole current stack got prefixed to every path parameter you use while calling the GUI.

You can nest a pushFolder() inside another pushFolder() - your path stack can be many levels deep. Just remember to call popFolder() the same number of times - the stack does get cleared after the end of draw() before the GUI starts drawing itself, but it's better not to rely on that.

Folder made by using the stack

gui.pushFolder("wave");
float frq = gui.slider("frequency");
float amp = gui.slider("amplitude");
gui.popFolder();

See the current stack for debugging

println(gui.getFolder());

see javadocs: pushFolder(), [popFolder()](https://krabcode.github.io/LazyGui/com/krab/lazy/LazyGui.html#popFolder()), [getFolder()](https://krabcode.github.io/LazyGui/com/krab/lazy/LazyGui.html#getFolder())

Hide and show anything

You can hide folders and single elements from code, while still receiving their values in code - the only change is visual. This is helpful when you have a loop for folders whose paths differ by the index, and you create too many of these folders and then want to hide some of them. You can also use this to hide the default 'options' or 'saves' folders.

gui.hide("myPath") // hide anything at this path (the prefix stack applies here like everywhere else)
gui.show("myPath") // reveal anything previously hidden at this path
gui.hideCurrentFolder() // hide the folder at the current path prefix stack
gui.showCurrentFolder() // show the folder at the current path prefix stack if it has been previously hidden 

Has a value changed last frame?

You can check whether a value has changed last frame with gui.hasChanged("myPath") and gui.hasChanged(). This can be useful when you don't want to do an expensive operation every frame but only when its controlling parameters change.

This works with single control elements, but it also works recursively through any child elements, so you can call it on a folder, and it will return true if any value nested under it has changed.

The result after a change is only true for one frame after a change and then gets reset to false for the next frame. These functions respect the current path stack. They do not initialize any new control elements or folders. Calling the function does not flip the value to false by itself.

See the UtilityMethods example which uses it to load a PFont whenever the user changes the font name or size.

Folder visuals

Runtime changes of what a folder row looks like in its parent window. This helps with organizing folders, especially with folder paths that differ only by the index inside a loop.

A folder will display a little 'on' light in its icon when at least one toggle inside the folder is set to true and its name matches one of the following:

A folder will display a name editable at runtime when there is a text control whose name matches one of the following:

Constructor settings

You can initialize your gui with an extra settings object to set various global defaults and affect startup and exit behavior. Loading a save overwrites these, but you can also disable loading on startup here.

Here's how to use it in a builder chain where the ordering does not matter (except for conflicting instructions):

gui = new LazyGui(this, new LazyGuiSettings()
        // set as false to not load anything on startup, true by default
    .setLoadLatestSaveOnStartup(false)

        // expects filenames like "1" or "auto.json", overrides 'load latest'    
    .setLoadSpecificSaveOnStartup("1")

        // controls whether to autosave, true by default
    .setAutosaveOnExit(false)    

        // windows will never 'restore' when loading a save (allowed once at startup by default)
    .setWindowRestoreNever()
);

Window restoration

When you load a save, the GUI will try to restore the window state to what it was when the save was made. This includes the position, size, and open/closed state of each window. There are three available modes, selected using the Constructor settings.

Live shader reloading

This GUI includes the (slightly out of scope) ShaderReloader class that watches your shader files as you edit them and re-compiles them when changes are made. If an error occurs during compilation, it keeps using the last compiled state and prints out the error to console.

Example using a fragment shader:

String shaderPath = "template.glsl";
PShader shader = ShaderReloader.getShader(shaderPath);
shader.set("time", (float) 0.001 * millis());
ShaderReloader.filter(shaderPath);

For shader compilation to work, ShaderReloader needs a reference to a PApplet, so in setup():

see: Shader Reloader javadocs

Input

This GUI also includes the Input utility that makes it easier to see whether any number of keys are currently pressed on the keyboard. Processing only shows you one key at a time while this utility keeps track of past events and can tell you whether any char or keyCode was just pressed, is currently held down or if it was just released.

Example detecting CTRL + SPACE with Input class static methods:

boolean isControlDown = Input.getCode(CONTROL).down;
boolean spaceWasJustPressed = Input.getChar(' ').pressed;
if(isControlDown && spaceWasJustPressed){
    println("ctrl + space pressed");
}

see: Input javadocs

Compatibility

LazyGui runs on all of these:

Dependencies

Further reading

How to contribute

How to compile and run this library