Fractal Softworks Forum

Please login or register.

Login with username, password and session length

Author Topic: Custom UI Guide : Necessary Basics  (Read 1616 times)

Kaysaar

  • Captain
  • ****
  • Posts: 389
    • View Profile
Custom UI Guide : Necessary Basics
« on: November 28, 2023, 09:38:13 AM »

When i was starting my journey with starsector modding i saw, that there were no guides about making UI. Same goes with knowledge, which was scattered. This is my attempt of trying to collect knowledge i gained so when someone starts their journey with UI

Here is a link to guide https://docs.google.com/document/d/1L0oP8hm8DEHkGudso7a4oonTnZ_-DQdWDtGjy3bLDrk/edit?usp=sharing
Feel free to contribiute in any way as there might be things that I got wrong

The knowledge here is based only on my experience, if you think I told sth wrong, or there is a better way, than what I presented, feel free to contribute. This is meant for all.
This one's for UI that uses CustomVisualDialogDelegate, forum link here
This guide is to show the basic of how to do your full custom UI from scratch telling process one by one

Designing:

Before you begin coding UI you first need to design it. As obvious as it might sound this is most important step, you plan your UI to be scalable with resolution, to avoid issues
Basically you test on two resolutions which i find most notorious

1280x960
1366x768


If the UI looks good on those both you can just be sure to test on 1920x1080 and if all  is fine and dandy, the UI is meeting criteria of being readable.
As an example of custom UI we will use tech tree UI (at least part of it) to just summarize how you can do such UI.



Here basically we have a checkbox with text that shows hovering info about sth, we will basically now dive into code how to make such one from scratch first to imitate its look then functionality so for example if i press it, something will appear.

Basics:
First what we need is to describe hierarchy of UI:



Main panel(CustomPanelAPI class) - > we can call it as foundations, all is bound to the main panel, we will use it to build our UI layer by layer. Of course you can directly create Tooltip (this will be discussed later what is tooltip) but in a much more complex UI this will only create a mess, that will be very hard to clean up later and for you as codder even harder to make adjustments in future.

To create such custom panel of our precious checkbox we use sth like this :

CustomPanelAPi customPanel = mainPanel.createCustomPanel(float width, float height, CustomPanelPlugin plugin);

CustomPanels are mainly used I say as “containers” of true UI.
TooltipMakerAPI is basically a class that is all about creating buttons, text and all that fancy stuff we always want in the UI.

TooltipMakerAPI customTooltip = customPanel.createUIElement(float width, float height, boolean withScroller)

Tooltips and custom panels are a little tricky as you can insert panels into tooltips and tooltips into panels.

Let’s start by going for the small component. First you insert panels into the tooltip (let’s call it tooltipA), so that you can have a more complex UI component which can use the scrollbar fairly easily. Then you can insert that tooltipA into the “container” panels so that not only can you place the container wherever you want, you can move it relatively easily.

Side note: If you were to place panel A directly inside panel B, the scroll bar no longer effects on panel A
   
In summary, tooltip is for creating whole UI stuff like buttons while panel is more of container where tooltip is placed , tooltip must be placed in container to work , but it can also hold other panels as UI components.

Important:
Panels you can only create from CustomPanelAPI, not the tooltip. So panel 2 and panel 3 must be initialized from panel 1 createCustomPanel method.
Tooltips MUST be initialized from PANELS you plan to place them in, so TOOLTIP 3  must be initialized from PANEL 3.

By making such a design you are able to create a much more advanced UI with ease.

Example of it you have right here:


Examples:

Button:

Okay so first to create such a button we will lay out layers. First custom panel and Tooltip. Because I want this button to basically cover the entire panel I’ll write on paint.


Code
buttonPanel=panel.createCustomPanel(AoTDUiComp.WIDTH_OF_TECH_PANEL - 1, AoTDUiComp.HEIGHT_OF_TECH_PANEL - 1, null);
TooltipMakerAPI vTT = buttonPanel.createUIElement(AoTDUiComp.WIDTH_OF_TECH_PANEL, AoTDUiComp.HEIGHT_OF_TECH_PANEL, false);

To insert button to tooltip we use method of addAreaChecbox
Code
vTT.addAreaCheckbox("", TechToResearch.Id, Misc.getNegativeHighlightColor(), Misc.getDarkHighlightColor(), Misc.getTooltipTitleAndLightHighlightColor(), AoTDUiComp.WIDTH_OF_TECH_PANEL, AoTDUiComp.HEIGHT_OF_TECH_PANEL, 0);

Pad here represents the amount of y pixels away from the last thing you placed on UI.
The colors here mean what color will highlight when hovered over this checkbox.

You can make if statement that changes this color depending on what you need (but its need to be done as it adds checkbox but with different parameters)

If you really wanna plan your UI, like for example i want text to be on top of tooltip you will use getPosition.intl(float x, float y) method. (Intl means from Top left corner)
Code
vTT.addPara(TechToResearch.Name, Color.ORANGE, 10f).getPosition().inTL(10, 5);

Also remember if a method returns for example an object of LabelAPI or TooltipMaker or ButtonAPI is always best to store those variables somewhere as you get more freedom over UI.

Now to make the button fully appear we do sth like this. x and y are coordinates there.
Code
buttonPanel.addUIElement(vTT).inTL(0, -1);
This code takes the vTT variable which is a tooltip from the code above, adding it into a button panel which creates the small component. Now we need to add it into the bigger container.

Code
panel.addUIElement(buttonPanel).inTL(x, y);
Or if you want to add to the tooltip if the container panel had one already created.
Code
tooltip.addComponent(buttonPanel).inTL(x, y);

The results?



About the blue lines around the box, it requires knowledge of OpenGL to pull this off. For simplicity the script you need is in the render method (This must be done when the panel is initialized !)
Code
void drawPanelBorder(CustomPanelAPI p) {
   GL11.glBegin(GL11.GL_LINE_LOOP);
   float x = p.getPosition().getX() - 5;
   float y = p.getPosition().getY();
   float w = p.getPosition().getWidth() + 10;
   float h = p.getPosition().getHeight();
   GL11.glVertex2f(x, y);
   GL11.glVertex2f(x + w, y);
   GL11.glVertex2f(x + w, y + h);
   GL11.glVertex2f(x, y + h);
   GL11.glEnd();
}

Note!: If you happen to use OpenGL on Panel that is inserted in tooltip with scrollbar, it will cause at some point render problems, like: Why those lines still render when i scrolled down and content of UI is not being shown. Read Stencil Mask Section!

So for now we have a simple area checkbox that highlights when you hover over it. But we can do more.


To make such tooltip appear on hovering we need to use method
Code
vTT.addTooltipToPrevious
NOTE: It must be used after you place the button. If you use it when you place Text, it will only appear if you hover over text !

This is the entire script for hovering UI.
Spoiler
Code
vTT.addTooltipToPrevious(new TooltipMakerAPI.TooltipCreator() {
   @Override
   public boolean isTooltipExpandable(Object tooltipParam) {
       return true;
   }


   @Override
   public float getTooltipWidth(Object tooltipParam) {
       return 300;
   }


   @Override
   public void createTooltip(TooltipMakerAPI tooltip, boolean expanded, Object tooltipParam) {
       tooltip.addSectionHeading("Technology Name", Alignment.MID, 10f);
       tooltip.addPara(TechToResearch.Name, 10f);
       tooltip.addSectionHeading("Time need to research", Alignment.MID, 10f);
       float days = TechToResearch.TimeToResearch-TechToResearch.daysSpentOnResearching;
       String d = " days";
       if(days<=1){
           d=" day";
       }
       AoTDFactionResearchManager manager = AoTDMainResearchManager.getInstance().getManagerForPlayer();
       if(!TechToResearch.isResearched()){
           tooltip.addPara((int)days+d+" to finish research", Misc.getTooltipTitleAndLightHighlightColor(), 10f);
       }
       else{
           tooltip.addPara("Researched!", Misc.getPositiveHighlightColor(), 10f);
       }


       tooltip.addSectionHeading("Unlocks", Alignment.MID, 10f);
       HashMap<ResearchRewardType, Boolean> haveThat = new HashMap<>();
       for (Map.Entry<String, ResearchRewardType> entry : TechToResearch.Rewards.entrySet()) {
           haveThat.put(entry.getValue(), true);
       }
       for (ResearchRewardType researchRewardType : haveThat.keySet()) {
           pickHeaderByReward(researchRewardType,tooltip);
           for (Map.Entry<String, ResearchRewardType> entry : TechToResearch.Rewards.entrySet()) {
               if (entry.getValue() == researchRewardType) {
                   createInfoFromType(entry.getValue(), entry.getKey(),tooltip);
               }
           }
       }
       if (!TechToResearch.ReqTechsToResearchFirst.isEmpty() || (TechToResearch.ReqItemsToResearchFirst != null && !TechToResearch.ReqItemsToResearchFirst.isEmpty())) {
           tooltip.addSectionHeading("Requirements", Alignment.MID, 10f);
       }
       if (!TechToResearch.ReqTechsToResearchFirst.isEmpty()) {
           tooltip.setParaInsigniaLarge();
           tooltip.addPara("Research", Color.ORANGE, 10f);
           tooltip.setParaFontDefault();


       }
       for (String s : TechToResearch.ReqTechsToResearchFirst) {
           if (s.equals("none")) continue;
           if(manager.haveResearched(s)){
               tooltip.addPara(manager.findNameOfTech(s).Name, Misc.getPositiveHighlightColor(), 10f);
           }
           else{
               tooltip.addPara(manager.findNameOfTech(s).Name, Misc.getNegativeHighlightColor(), 10f);
           }


       }


       if (TechToResearch.ReqItemsToResearchFirst != null && !TechToResearch.ReqItemsToResearchFirst.isEmpty()) {
           tooltip.setParaInsigniaLarge();
           LabelAPI title = tooltip.addPara("Items", Color.ORANGE, 10f);
           tooltip.setParaFontDefault();
           for (Map.Entry<String, Integer> entry : TechToResearch.ReqItemsToResearchFirst.entrySet()) {
               CustomPanelAPI panel = mainPanel.createCustomPanel(300,60,null);
               TooltipMakerAPI tooltipMakerAPI = panel.createUIElement(60,60,false);
               TooltipMakerAPI labelTooltip = panel.createUIElement(220,60,false);
               LabelAPI labelAPI1 = null;
               if(Global.getSettings().getCommoditySpec(entry.getKey())!=null){
                   tooltipMakerAPI.addImage(Global.getSettings().getCommoditySpec(entry.getKey()).getIconName(),60,60,10f);
                   labelAPI1 = labelTooltip.addPara(Global.getSettings().getCommoditySpec(entry.getKey()).getName()+" : "+entry.getValue(),10f);
               }
               if(Global.getSettings().getSpecialItemSpec(entry.getKey())!=null){
                   tooltipMakerAPI.addImage(Global.getSettings().getSpecialItemSpec(entry.getKey()).getIconName(),60,60,10f);
                   labelAPI1 = labelTooltip.addPara(Global.getSettings().getSpecialItemSpec(entry.getKey()).getName()+" : "+entry.getValue(),10f);
               }
               if(manager.haveMetReqForItem(entry.getKey(),entry.getValue())||manager.getResearchOptionFromRepo(TechToResearch.Id).havePaidForResearch){
                   labelAPI1.setColor(Misc.getPositiveHighlightColor());
               }
               else{
                   labelAPI1.setColor(Misc.getNegativeHighlightColor());
               }
               if(TechToResearch.isResearched){
                   labelAPI1.setColor(Misc.getPositiveHighlightColor());
               }
               panel.addUIElement(tooltipMakerAPI).inTL(-10,-20);
               panel.addUIElement(labelTooltip).inTL(60,5);
               tooltip.addCustom(panel,10f);
           }


       }
       if(TechToResearch.otherReq!=null){
           tooltip.setParaInsigniaLarge();
           tooltip.addPara("Other", Color.ORANGE, 10f);
           tooltip.setParaFontDefault();
           if(!TechToResearch.metOtherReq){
               tooltip.addPara(TechToResearch.otherReq.two+"\n",Misc.getNegativeHighlightColor(),10f);
           }
           else{
               tooltip.addPara(TechToResearch.otherReq.two+"\n",Misc.getPositiveHighlightColor(),10f);
           }


       }


   }
}, TooltipMakerAPI.TooltipLocation.RIGHT);
[close]

Image:

Okey i need to explain one trick here with images.
addImage method is void method, so you get nothing. So you can't manipulate placement of image, unless….
unless you make special panel with tooltip which sole purpose is just to be anchor for image.
You place that image in tooltip, which you then place on that special panel and you got yourself image, that can easily be relocated with location of that special panel.
(Or you can use getPrev method in TooltipMakerAPI directly after you place image with addImage - added because someone was feeling insulted that guide does not include this method)

Other Methods

addSectionHeading is responsible for adding this part


setParaInsigniaLarge makes string that were placed with addPara method much bigger


Using Stencil Mask

To avoid blue lines rendered with drawPanelBorder like on that image below:



You need to use Stencil Mask.

First before you even start using stencil you need to include this in advance method.
Code
 glClearStencil(0);
glStencilMask(0xff);

Here is function for : drawmask as we will be using that below:
Code
 void drawmask(CustomPanelAPI p) {
        GL11.glBegin(GL_QUADS);
        float x = p.getPosition().getX() - 6;
        float y = p.getPosition().getY();
        float w = p.getPosition().getWidth() + 11;
        float h = p.getPosition().getHeight();
        GL11.glVertex2f(x, y);
        GL11.glVertex2f(x + w, y);
        GL11.glVertex2f(x + w, y + h);
        GL11.glVertex2f(x, y + h);
        GL11.glEnd();
    }

Then moving on to render method section.
Code
  glClear(GL_STENCIL_BUFFER_BIT);
            glColorMask(false, false, false, false); //disable colour
            glEnable(GL_STENCIL_TEST); //enable stencil
            openGlUtilis.drawmask(panel1);
            glStencilFunc(GL_ALWAYS, 1, 0xff); // Do not test the current value in the stencil buffer, always accept any value on there for drawing
            glStencilMask(0xff);
            glStencilOp(GL_REPLACE, GL_REPLACE, GL_REPLACE); // Make every test succeed
            openGlUtilis.drawmask(panel1);
            glStencilOp(GL_KEEP, GL_KEEP, GL_KEEP); // Make sure you will no longer (over)write stencil values, even if any test succeeds
            glColorMask(true, true, true, true); // Make sure we draw on the backbuffer again.
            glStencilFunc(GL_EQUAL, 1, 0xFF); // Now we will only draw pixels where the corresponding stencil buffer value equals 1

//here you have code for drawing panels for example with drawPanelBorder we mentioned , that will be located in stencil mask

         glDisable(GL_STENCIL_TEST);

Tips

Try to divide your UI into smaller components, therefore it will be easier for you, to navigate in your code.
Example of such is for example my own UIPanel:

Code
package data.kaysaar.aotd.vok.ui.components;

import com.fs.starfarer.api.ui.CustomPanelAPI;
import com.fs.starfarer.api.ui.TooltipMakerAPI;

import java.awt.*;

public class UiPanel implements AoTDUiComp {
    public CustomPanelAPI mainPanel;
    public CustomPanelAPI panel;
    public TooltipMakerAPI tooltip;

    public void init(CustomPanelAPI mainPanel, CustomPanelAPI panelAPI, TooltipMakerAPI tooltipMakerAPI) {
        this.mainPanel = mainPanel;
        panel = panelAPI;
        tooltip = tooltipMakerAPI;
    }

    public void createUI() {

    }

    @Override
    public void createUI(float x, float y) {

    }

    public void placeTooltip(float x, float y) {
        panel.addUIElement(tooltip).inTL(x, y);
    }

    public void placeSubPanel(float x, float y) {
        mainPanel.addComponent(panel).inTL(x, y);
    }

    @Override
    public void render(Color colorOfRender, float alphamult) {

    }
}

Summary:
For the rest of the UI what you need is imagination of  how you wanna place components and how it's gonna look like. This guide only shows very basics of UI -> basically how to treat CustomPanelAPI and TooltipMakerAPI.
« Last Edit: November 29, 2023, 12:18:16 AM by Kaysaar »
Logged

nathan67003

  • Commander
  • ***
  • Posts: 158
  • Excellent imagination, mediocre implementation.
    • View Profile
Re: Custom UI Guide : Necessary Basics
« Reply #1 on: November 29, 2023, 06:46:24 AM »

Praise be. More guides is always a great thing.
Logged
I have some ideas but can't sprite worth a damn and the ideas imply really involved stuff which I've no clue how to even tackle.

Alex

  • Administrator
  • Admiral
  • *****
  • Posts: 24146
    • View Profile
Re: Custom UI Guide : Necessary Basics
« Reply #2 on: January 10, 2024, 07:56:30 PM »

Stickied this, very nice work!
Logged