• Hi Guest!

    We are extremely excited to announce the release of our first Beta1.1 and the first release of our Public AddonKit!
    To participate in the Beta, a subscription to the Entertainer or Creator Tier is required. For access to the Public AddonKit you must be a Creator tier member. Once subscribed, download instructions can be found here.

    Click here for information and guides regarding the VaM2 beta. Join our Discord server for more announcements and community discussion about VaM2.
  • Hi Guest!

    VaM2 Resource Categories have now been added to the Hub! For information on posting VaM2 resources and details about VaM2 related changes to our Community Forums, please see our official announcement here.
CustomTabUI

Plugins + Scripts CustomTabUI

Download [<1 MB]

14mhz

Well-known member
Joined
Oct 23, 2024
Messages
201
Solutions
7
Reactions
1,659
14mhz submitted a new resource:

CustomTabUI - This simple plugin for Custom tabUI implementation.

CustomTabUI
● This simple plugin for new style tab UI implementation.
● There is no need to remove UI elements when changing tabs.
● Supports tabs expressed as icons.
● This library is free to modify and distribute, but credit is required.
- CustomTabUI.cs is in 14mhz.Plugin-CustomTabUI.1.var
- CustomTabUIExample1.cs is in 14mhz.Plugin-CustomTabUIExample.1.var
- CustomTabUIExample2.cs is in 14mhz.Plugin-CustomTabUIExample.1.var
- CustomTabUIExample3.cs is in...

Read more about this resource...
 
There is a bug in v0.2. (in case JSONStorableAction, ie, initButton)
Please use the code below until v0.3 is patched.
Also, code below that support Tab Traversing.
with the announcement of a new release, attachments have been removed.
C#:
CustomTabUI tui = new CustomTabUI(this, CreateUIElement, "A");
Tab a = tui.newTab(this, "TabA");
{
    a.add(initSlider("SliderA"));
    a.add(initToggle("ToggleA"));
}
Tab b = tui.newTab(this, "TabB");
{
    b.add(initSlider("SliderB"));
    b.add(initToggle("ToggleB"));
}
---- full traverse ----
foreach (KeyValuePair<String, Tab>pair in tui.getTabs())
{
    String    key = pair.Key;    // TabA, TabB key
    Tab       tab = pair.Value;  // TabA, TabB instance
    for (int i = 0; i < tab.elements.Count; i++)
    {
         object o = elements[i]; // JSONStorableParam or JSONStorableAction
    }
}
---- key traverse ----
Tab a = tui.getTab("TabA");
Tab b = tui.getTab("TabB");
JSONStorableFloat j == a.get("SliderA") == tui.getElement("TabA", "SliderA") as JSONStorableFloat;
JSONStorableBool  j == a.get("ToggleA") == tui.getElement("TabA", "ToggleA") as JSONStorableBool;
JSONStorableFloat j == b.get("SliderB") == tui.getElement("TabB", "SliderB") as JSONStorableFloat;
JSONStorableBool  j == b.get("ToggleB") == tui.getElement("TabB", "ToggleB") as JSONStorableBool;
---- also, dynamic obtaining UIDynamic via JSONStorable ----
UIDynamicSlider      u = Tools.JSONStorable2UIDynamic(instance of JSONStorableFloat) as UIDynamicSlider;
UIDynamicToggle      u = Tools.JSONStorable2UIDynamic(instance of JSONStorableBool) as UIDynamicToggle;
UIDynamicColorPicker u = Tools.JSONStorable2UIDynamic(instance of JSONStorableColor) as UIDynamicToggle;
UIDynamicTextField   u = Tools.JSONStorable2UIDynamic(instance of JSONStorableString) as UIDynamicTextField;
UIDynamicPopup       u = Tools.JSONStorable2UIDynamic(instance of JSONStorableStringChooser) as UIDynamicPopup;
UIDynamicButton      u = Tools.JSONStorable2UIDynamic(instance of JSONStorableAction) as UIDynamicButton;
 
Last edited:
Hi, I'm using this for a project, it's great... thanks. I have a couple of suggestions for it;
  • I found it a bit tricky to handle dynamic controls (eg adding new controls when user clicks "Add") as the tabs don't redraw properly (the background size does not grow and the controls are not hidden immediately if the user is not on the tab the controls are being added to). The workaround I had was to call active() on the tab which forces a redraw which was fine. But, there's no property exposed to know what tab the user has open - so it would be good to have a "redraw()" function on the tab, and expose a property to know which tab is active.
  • would be great to have a hide() / show() function to hide the entire control or an individual tab without having to destroy it entirely
 
Hi, I'm using this for a project, it's great... thanks. I have a couple of suggestions for it;
  • I found it a bit tricky to handle dynamic controls (eg adding new controls when user clicks "Add") as the tabs don't redraw properly (the background size does not grow and the controls are not hidden immediately if the user is not on the tab the controls are being added to). The workaround I had was to call active() on the tab which forces a redraw which was fine. But, there's no property exposed to know what tab the user has open - so it would be good to have a "redraw()" function on the tab, and expose a property to know which tab is active.
  • would be great to have a hide() / show() function to hide the entire control or an individual tab without having to destroy it entirely
Coincidentally, developers think in much the same way.
I was developing a dynamic add/remove feature. :LOL:
Us who think the same way are called The DEVELOPER, Welcome. Friend
※ As per your request, the name of the repaint method has been changed to redraw().
 
Last edited:
20250813 CustomLayoutUI.gif

CustomLayoutUI, comming soon ...
 
Hi, as discussed in today's message, I would welcome it very much if there was an option to have a (very) compact slider. The standard slider uses half a line and quite some height - I think it would be ideal to have one that is only one quarter of a line, such as in Flexit or Shakeit by CheesyFX, but without the -1, +0.1 and range buttons. Just the slider, a title and the value (editable number). If a default button is still possible, that would be helpful.

This is the small slider version in Shakeit:
1757361415366.png


These are the default sliders in my plugin (consume too much space):
1757361488811.png


I am however not sure how to use your plugin, or rather the UI layouts it generates.
Is it intended to copy the code from the .cs and integrate it into my own plugin, and then edit it? Sorry for being stupid, and thanks for the initiative!
 
Hi, as discussed in today's message, I would welcome it very much if there was an option to have a (very) compact slider. The standard slider uses half a line and quite some height - I think it would be ideal to have one that is only one quarter of a line, such as in Flexit or Shakeit by CheesyFX, but without the -1, +0.1 and range buttons. Just the slider, a title and the value (editable number). If a default button is still possible, that would be helpful.

This is the small slider version in Shakeit:
View attachment 523497

These are the default sliders in my plugin (consume too much space):
View attachment 523499

I am however not sure how to use your plugin, or rather the UI layouts it generates.
Is it intended to copy the code from the .cs and integrate it into my own plugin, and then edit it? Sorry for being stupid, and thanks for the initiative!
Please try using the code below. CustomLayoutUI and CustomUI are required.
C#:
public override void Init()
{// generate a 4×10 (column x row) cell.
    CustomLayoutUI layout = CustomLayoutUI.create(this, CreateUIElement, "TestLayout", 0, 480).newFullLineUI().setLayout(4, 10);
    for (int r = 0; r < layout.row; r++)
    for (int c = 0; c < layout.col; c++)
    {
        layout.addUIComponent(CustomUI.addSliderSimple(this, true, "Slider" + c.ToString() + r.ToString(), 0, -10, +10)
        .setLabelTextWeight(0.5f).setCallback(callback).register(), c, r); // c : column, r : row
    }
}
Snap2.jpg
If the slider name is too long,
C#:
public override void Init()
{// generate a 4×10 (column x row) cell.
    CustomLayoutUI layout = CustomLayoutUI.create(this, CreateUIElement, "TestLayout", 0, 600).newFullLineUI().setLayout(4, 10);
    for (int r = 0; r < layout.row; r++)
    for (int c = 0; c < layout.col; c++)
    {
        String             name = "Slider" + c.ToString() + r.ToString();
        BaseLabelUI        ui01 = CustomUI.addLabel(this, true, name).setTextFontSize(20).setBackgroundColor(Color.black).setTextColor(Color.white);
        BaseSliderSimpleUI ui02 = CustomUI.addSliderSimple(this, true, name, 0, -10, +10).setLabelTextWeight(0.03f).setSliderWeight(0.7f).setCallback(callback).register();
        CustomLayoutUI     cell = CustomLayoutUI.create(this, CreateUIElement, "Cell").newHalfLineUI(true).setLayout(1, 2).setRowWeight(0f,0.4f,1f);
        cell.addUIComponent(ui01, 0, 0).addUIComponent(ui02, 0, 1);
        layout.addUIComponent(cell, c, r);
    }
}
Snap3.jpg
 
Last edited:
Please try using the code below. CustomLayoutUI and CustomUI are required.
C#:
public override void Init()
{// generate a 4×10 (column x row) cell.
    CustomLayoutUI layout = CustomLayoutUI.create(this, CreateUIElement, "TestLayout", 0, 480).newFullLineUI().setLayout(4, 10);
    for (int r = 0; r < layout.row; r++)
    for (int c = 0; c < layout.col; c++)
    {
        layout.addUIComponent(CustomUI.addSliderSimple(this, true, "Slider" + c.ToString() + r.ToString(), 0, -10, +10)
        .setLabelTextWeight(0.5f).setCallback(callback).register(), c, r); // c : column, r : row
    }
}
View attachment 523651
If the slider name is too long,
C#:
public override void Init()
{// generate a 4×10 (column x row) cell.
    CustomLayoutUI layout = CustomLayoutUI.create(this, CreateUIElement, "TestLayout", 0, 600).newFullLineUI().setLayout(4, 10);
    for (int r = 0; r < layout.row; r++)
    for (int c = 0; c < layout.col; c++)
    {
        String             name = "Slider" + c.ToString() + r.ToString();
        BaseLabelUI        ui01 = CustomUI.addLabel(this, true, name).setTextFontSize(20).setBackgroundColor(Color.black).setTextColor(Color.white);
        BaseSliderSimpleUI ui02 = CustomUI.addSliderSimple(this, true, name, 0, -10, +10).setLabelTextWeight(0.03f).setSliderWeight(0.7f).setCallback(callback).register();
        CustomLayoutUI     cell = CustomLayoutUI.create(this, CreateUIElement, "Cell").newHalfLineUI(true).setLayout(1, 2).setRowWeight(0f,0.4f,1f);
        cell.addUIComponent(ui01, 0, 0).addUIComponent(ui02, 0, 1);
        layout.addUIComponent(cell, c, r);
    }
}
View attachment 523653
Thats pretty cool, looks amazing! I see where the classes are in the linked .cs files (where it gets the formatting from), but I have no idea how to include this into an existing plugin. I suggest you give a few hints for non pro developers (like me) to understand how to best use your content? Thanks again!
 
Thats pretty cool, looks amazing! I see where the classes are in the linked .cs files (where it gets the formatting from), but I have no idea how to include this into an existing plugin. I suggest you give a few hints for non pro developers (like me) to understand how to best use your content? Thanks again!
In MacGruber_Utils, the Slider is defined as follows: (It’s probably the function you’re using.)
C#:
public static JSONStorableFloat SetupSliderFloat(MVRScript script, string label, float defaultValue, float minValue, float maxValue, bool rightSide)
Similarly, in CustomUI, the Slider is defined like this:
C#:
public static BaseSliderSimpleUI addSliderSimple(MVRScript script, bool isLeft, String name, float value, float min, float max)
The first thing to do is to replace the existing bulky slider with a simple one.
I think it would be best to change just one slider function in your project first,
check if it works well, and then gradually make further changes one by one.
Let me know if you have any other questions.
If this process succeeds, I will let you know the next step
 
Last edited:
In MacGruber_Utils, the Slider is defined as follows: (It’s probably the function you’re using.)
C#:
public static JSONStorableFloat SetupSliderFloat(MVRScript script, string label, float defaultValue, float minValue, float maxValue, bool rightSide)
Similarly, in CustomUI, the Slider is defined like this:
C#:
public static BaseSliderSimpleUI addSliderSimple(MVRScript script, bool isLeft, String name, float value, float min, float max)
The first thing to do is to replace the existing bulky slider with a simple one.
I think it would be best to change just one slider function in your project first,
check if it works well, and then gradually make further changes one by one.
Let me know if you have any other questions.
If this process succeeds, I will let you know the next step
Thank you very much!! It does work, although I do have a slightly different UI system, because I already have a crude tab system and I need to add/destroy the sliders when the tab is activated/deactivated, but not the JSONStorable.

Currently, I use for my sliders:

C#:
CreateSlider(erectionmorph1_min_JSON, false);

The JSONStorable is defined and registered in the Init() part.

If I use an adapted form of your code like this:
C#:
//Create slider and bind it to existing JSONStorableFloat
var slider = CustomUI.addSliderSimple(
    this,
    true,
    erectionmorph1_min_JSON.name,          // name
    erectionmorph1_min_JSON.val,           // start value
    erectionmorph1_min_JSON.min,           // min
    erectionmorph1_min_JSON.max            // max
)
.setLabelTextWeight(0.5f)
.register();

//Callback function
slider.setCallback(val => erectionmorph1_min_JSON.val = val);
erectionmorph1_min_JSON.setCallbackFunction = (float val) => slider.setValue(val);

it will look like this:

1757532872684.png


I can make the text label shorter, no problem. It's amost perfect, only the default value function is missing. Do you have any idea if this could be added?
 
Thank you very much!! It does work, although I do have a slightly different UI system, because I already have a crude tab system and I need to add/destroy the sliders when the tab is activated/deactivated, but not the JSONStorable.

Currently, I use for my sliders:

C#:
CreateSlider(erectionmorph1_min_JSON, false);

The JSONStorable is defined and registered in the Init() part.

If I use an adapted form of your code like this:
C#:
//Create slider and bind it to existing JSONStorableFloat
var slider = CustomUI.addSliderSimple(
    this,
    true,
    erectionmorph1_min_JSON.name,          // name
    erectionmorph1_min_JSON.val,           // start value
    erectionmorph1_min_JSON.min,           // min
    erectionmorph1_min_JSON.max            // max
)
.setLabelTextWeight(0.5f)
.register();

//Callback function
slider.setCallback(val => erectionmorph1_min_JSON.val = val);
erectionmorph1_min_JSON.setCallbackFunction = (float val) => slider.setValue(val);

it will look like this:

View attachment 524065

I can make the text label shorter, no problem. It's amost perfect, only the default value function is missing. Do you have any idea if this could be added?

Wow, nicely done!
Next, we'll build the entire UI using a cell-based CustomLayoutUI. Since we need a total of 16 sliders, we'll arrange them in a 4×4 grid.
C#:
String[]title = {"Penis Length Flaccid", "Penis Length Erect", "Shaft Grith Flaccid", "Shaft Grith Erect",
                "Curve Up/Down Flaccid", "Curve Up/Down Erect", "Bend Left/Right Flaccid", "Bend Left/Right Erect",
                "Base Up/Down Flaccid", "Base Up/Down Erect", "Small Scrotum Flaccid", "Small Scrotum Erect",
                "Right Testicle Up Flaccid", "Right Testicle Up Erect", "Left Testicle Up Flaccid", "Left Testicle Up Erect" };
float[][]value = new float[][] { // init value, min value, max value, edit this part !
                new float[] {1f, -10f, 10f}, new float[] {0f, -10f, 10f}, new float[] {0f, -10f, 10f}, new float[] {0f, -10f, 10f},
                new float[] {0f, -10f, 10f}, new float[] {2f, -10f, 10f}, new float[] {0f, -10f, 10f}, new float[] {0f, -10f, 10f},
                new float[] {0f, -10f, 10f}, new float[] {0f, -10f, 10f}, new float[] {3f, -10f, 10f}, new float[] {0f, -10f, 10f},
                new float[] {0f, -10f, 10f}, new float[] {0f, -10f, 10f}, new float[] {0f, -10f, 10f}, new float[] {4f, -10f, 10f}};

public override void Init()
{// generate a 4×4 (column x row) cell.
    CustomLayoutUI layout = CustomLayoutUI.create(this, CreateUIElement, "TestLayout", 0, 300).newFullLineUI().enableLayoutMode(false).setLayout(4, 4); // 300 is height as fullline ui (4x4)
    for (int r = 0; r < layout.row; r++)
    for (int c = 0; c < layout.col; c++)
    {// Since all components included in the CustomLayoutUI are owned by the layout itself,
     // the concepts of "Left" and "Right" have no meaningful relevance.
        String             name = title[r*4 + c];
        float            []_val = value[r*4 + c];
        BaseLabelUI        ui01 = CustomUI.addLabel(this, true, name).setTextFontSize(20).setBackgroundColor(Color.black).setTextColor(Color.white);  // This is not saved to the scene, so it does not need to be register().
        BaseSliderSimpleUI ui02 = CustomUI.addSliderSimple(this, true, name, _val[0], _val[1], _val[2]).setLabelTextWeight(0.03f).setSliderWeight(0.7f).setCallback(callbackEvent).register();
        CustomLayoutUI     cell = CustomLayoutUI.create(this, CreateUIElement, "Cell").newHalfLineUI(true).enableLayoutMode(false).setLayout(1, 2).setRowWeight(0f,0.4f,1f); // halfline ui (1x2), label (0~0.4f), slider (0.4f~0.6f)
        cell.addUIComponent(ui01, 0, 0).addUIComponent(ui02, 0, 1);
        layout.addUIComponent(cell, c, r);
    }
}

void callbackEvent(String name, float value)
{
    if (true) SuperController.LogMessage($"[INF] callback [{name}] [{value}]"); // for debug, event check
    if (false) {}
    else if (name == title[0]) { /* do someting */ }
    else if (name == title[1]) { /* do someting */ }
    else if (name == title[2]) { /* do someting */ }
    else if (name == title[3]) { /* do someting */ }
    else if (name == title[4]) { /* do someting */ }
    else if (name == title[5]) { /* do someting */ }
    else if (name == title[6]) { /* do someting */ }
    else if (name == title[7]) { /* do someting */ }
    else if (name == title[8]) { /* do someting */ }
    else if (name == title[9]) { /* do someting */ }
    else if (name == title[10]) { /* do someting */ }
    else if (name == title[11]) { /* do someting */ }
    else if (name == title[12]) { /* do someting */ }
    else if (name == title[13]) { /* do someting */ }
    else if (name == title[14]) { /* do someting */ }
    else if (name == title[15]) { /* do someting */ }
}
Once the implementation is complete, the layout should appear as shown in the illustration below.
Snap3.jpg

If you've made it this far successfully, let's move on to attaching it to the Tab together.
 
Last edited:
Wow, nicely done!
Next, we'll build the entire UI using a cell-based CustomLayoutUI. Since we need a total of 16 sliders, we'll arrange them in a 4×4 grid.
C#:
String[]title = {"Penis Length Flaccid", "Penis Length Erect", "Shaft Grith Flaccid", "Shaft Grith Erect",
                "Curve Up/Down Flaccid", "Curve Up/Down Erect", "Bend Left/Right Flaccid", "Bend Left/Right Erect",
                "Base Up/Down Flaccid", "Base Up/Down Erect", "Small Scrotum Flaccid", "Small Scrotum Erect",
                "Right Testicle Up Flaccid", "Right Testicle Up Erect", "Left Testicle Up Flaccid", "Left Testicle Up Erect" };
float[][]value = new float[][] { // init value, min value, max value, edit this part !
                new float[] {1f, -10f, 10f}, new float[] {0f, -10f, 10f}, new float[] {0f, -10f, 10f}, new float[] {0f, -10f, 10f},
                new float[] {0f, -10f, 10f}, new float[] {2f, -10f, 10f}, new float[] {0f, -10f, 10f}, new float[] {0f, -10f, 10f},
                new float[] {0f, -10f, 10f}, new float[] {0f, -10f, 10f}, new float[] {3f, -10f, 10f}, new float[] {0f, -10f, 10f},
                new float[] {0f, -10f, 10f}, new float[] {0f, -10f, 10f}, new float[] {0f, -10f, 10f}, new float[] {4f, -10f, 10f}};

public override void Init()
{// generate a 4×4 (column x row) cell.
    CustomLayoutUI layout = CustomLayoutUI.create(this, CreateUIElement, "TestLayout", 0, 300).newFullLineUI().enableLayoutMode(false).setLayout(4, 4); // 300 is height as fullline ui (4x4)
    for (int r = 0; r < layout.row; r++)
    for (int c = 0; c < layout.col; c++)
    {// Since all components included in the CustomLayoutUI are owned by the layout itself,
     // the concepts of "Left" and "Right" have no meaningful relevance.
        String             name = title[r*4 + c];
        float            []_val = value[r*4 + c];
        BaseLabelUI        ui01 = CustomUI.addLabel(this, true, name).setTextFontSize(20).setBackgroundColor(Color.black).setTextColor(Color.white);  // This is not saved to the scene, so it does not need to be register().
        BaseSliderSimpleUI ui02 = CustomUI.addSliderSimple(this, true, name, _val[0], _val[1], _val[2]).setLabelTextWeight(0.03f).setSliderWeight(0.7f).setCallback(callbackEvent).register();
        CustomLayoutUI     cell = CustomLayoutUI.create(this, CreateUIElement, "Cell").newHalfLineUI(true).enableLayoutMode(false).setLayout(1, 2).setRowWeight(0f,0.4f,1f); // halfline ui (1x2), label (0~0.4f), slider (0.4f~0.6f)
        cell.addUIComponent(ui01, 0, 0).addUIComponent(ui02, 0, 1);
        layout.addUIComponent(cell, c, r);
    }
}

void callbackEvent(String name, float value)
{
    if (true) SuperController.LogMessage($"[INF] callback [{name}] [{value}]"); // for debug, event check
    if (false) {}
    else if (name == title[0]) { /* do someting */ }
    else if (name == title[1]) { /* do someting */ }
    else if (name == title[2]) { /* do someting */ }
    else if (name == title[3]) { /* do someting */ }
    else if (name == title[4]) { /* do someting */ }
    else if (name == title[5]) { /* do someting */ }
    else if (name == title[6]) { /* do someting */ }
    else if (name == title[7]) { /* do someting */ }
    else if (name == title[8]) { /* do someting */ }
    else if (name == title[9]) { /* do someting */ }
    else if (name == title[10]) { /* do someting */ }
    else if (name == title[11]) { /* do someting */ }
    else if (name == title[12]) { /* do someting */ }
    else if (name == title[13]) { /* do someting */ }
    else if (name == title[14]) { /* do someting */ }
    else if (name == title[15]) { /* do someting */ }
}
Once the implementation is complete, the layout should appear as shown in the illustration below.
View attachment 524188
If you've made it this far successfully, let's move on to attaching it to the Tab together.
Thanks again, I will consider this, but I think I would rather only go forward if we can somehow find a solution to the missing "default value" functionality.
This is rather important (in contrast to the +1, +0.1 and range buttons) and users need this to reset single morph values.. Not all of them are 0 by default (as they are morphs).
 
Thanks again, I will consider this, but I think I would rather only go forward if we can somehow find a solution to the missing "default value" functionality.
This is rather important (in contrast to the +1, +0.1 and range buttons) and users need this to reset single morph values.. Not all of them are 0 by default (as they are morphs).
It’s also possible to create a button that provides default values, as shown in the image below.
Snap1.jpg
 
Last edited:
Hi, could you please help me with destroying the ui elements correctly? This is how it looks if active:

1757748973292.png


I try the following to remove all CustomUI elements (BaseSliderSimpleUI elements):
C#:
CustomUI.closeAll();

if (layout != null)
{
   UnityEngine.Object.Destroy(layout.gameObject);
   layout = null;
}

but this will always leave an empty box / placeholder (maybe some registered slots that remain). Do I miss another function to deregister the sliders?
This is how the UI looks after calling my destroy code:

1757749048454.png


Thank you (and let me know if you get annoyed by my questions please)!
 
Hi, could you please help me with destroying the ui elements correctly? This is how it looks if active:

View attachment 524888

I try the following to remove all CustomUI elements (BaseSliderSimpleUI elements):
C#:
CustomUI.closeAll();

if (layout != null)
{
   UnityEngine.Object.Destroy(layout.gameObject);
   layout = null;
}

but this will always leave an empty box / placeholder (maybe some registered slots that remain). Do I miss another function to deregister the sliders?
This is how the UI looks after calling my destroy code:

View attachment 524889

Thank you (and let me know if you get annoyed by my questions please)!
1. You can simply call CustomLayoutUI.closeAll();
2. Not at all! Feel free to ask anything—I'll help as much as I can. :D
 
1. You can simply call CustomLayoutUI.closeAll();
2. Not at all! Feel free to ask anything—I'll help as much as I can. :D
Perfect, thanks!!

I will go for a three col layout here, because that makes most sense. Also found the Default Button and some more formatting options.

1757758451035.png


How would you do small spacers between the sliders? Would you use a 5 column layout or rather some padding or so?
 
Back
Top Bottom