ee6a1dba28
Copybara import of the project: -- a1297d997d0b3cc624bbd1e704ffb7fbb2c5db9d by Emo Todorov <etodorov@gmail.com>: UI imrovements When OpenGL buffer size is too small to hold entire UI with all sections open, older sections (as determined by latest mouse click) are automatically closed as needed. To test this new functionality, uncomment the test near the top of mjui_resize(). UI items now have userid, which can be used for event handling. It is set through the (new) last field of mjuiDef, called otherint. Sections titles can now have a checkmark - in which case they are rendered differently. Set via mjuiDef.otherint: 0- none (as before), 1- rendering changed but box not shown, 2- box shown. This can be used to label sections of special interest to the application. -- b0405bfb3005586ef1d2656ea9cc43c608225927 by Emo Todorov <etodorov@gmail.com>: more ui improvements Implemented mjPRESERVE state for adding sections and separators, so you don't have to keep track of old states. simulate.cc is now modified to use this new functionality. I also made the separators in the Physics section collapsible. If you don't like it, change them back to state 1. Also added functionality for handling section checkboxes. -- 10f4c49be62a6605a38b7c0c7bfae1629024ba31 by Emo Todorov <etodorov@gmail.com>: even more ui improvements Selection boxes that extend below the current UI height are finally rendered in full. Sections and collapsible separators now have rounded corners. The radius can be controlled from theme spacing. More theme color settings were added. In particular, color gradients can now be disabled (the two end colors are set independently). -- 96c2dc601a25d31c93b21f8e089d349e729e829e by Emo Todorov <etodorov@gmail.com>: final ui improvements Cleaned up the code a bit. Added more control for color gradients. -- c9eddb679f36df3f0745d37e3fa90c4e66ee6b98 by Emo Todorov <etodorov@gmail.com>: bug fix Fixed some rendering bugs in the new functionality. COPYBARA_INTEGRATE_REVIEW=https://github.com/google-deepmind/mujoco/pull/1749 from emotodorov:ui_improve ddfc21dbcf0482b8f0d5eacab21847c302f71440 PiperOrigin-RevId: 646060325 Change-Id: Idb27aae7cd0311d00f26387bcadf5891175e867d
2968 lines
80 KiB
C
2968 lines
80 KiB
C
// Copyright 2021 DeepMind Technologies Limited
|
|
//
|
|
// Licensed under the Apache License, Version 2.0 (the "License");
|
|
// you may not use this file except in compliance with the License.
|
|
// You may obtain a copy of the License at
|
|
//
|
|
// http://www.apache.org/licenses/LICENSE-2.0
|
|
//
|
|
// Unless required by applicable law or agreed to in writing, software
|
|
// distributed under the License is distributed on an "AS IS" BASIS,
|
|
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
// See the License for the specific language governing permissions and
|
|
// limitations under the License.
|
|
|
|
#include "ui/ui_main.h"
|
|
|
|
#include <math.h>
|
|
#include <stdio.h>
|
|
#include <string.h>
|
|
|
|
#include <mujoco/mjmacro.h>
|
|
#include <mujoco/mujoco.h>
|
|
#include "engine/engine_array_safety.h"
|
|
#include "render/glad/glad.h"
|
|
|
|
//------------------------------------ Default themes ----------------------------------------------
|
|
|
|
// theme spacing 0 : tight
|
|
static const mjuiThemeSpacing themeSpacing0 = {
|
|
270, // total
|
|
15, // scroll
|
|
120, // label
|
|
8, // section
|
|
6, // cornersect
|
|
6, // cornersep
|
|
4, // itemside
|
|
4, // itemmid
|
|
4, // itemver
|
|
8, // texthor
|
|
4, // textver
|
|
30, // linescroll
|
|
4 // samples
|
|
};
|
|
|
|
|
|
// theme spacing 1 : wide
|
|
static const mjuiThemeSpacing themeSpacing1 = {
|
|
310, // total
|
|
15, // scroll
|
|
120, // label
|
|
10, // section
|
|
10, // cornersect
|
|
10, // cornersep
|
|
7, // itemside
|
|
7, // itemmid
|
|
7, // itemver
|
|
10, // texthor
|
|
5, // textver
|
|
30, // linescroll
|
|
4 // samples
|
|
};
|
|
|
|
|
|
// theme color 0 : default
|
|
static const mjuiThemeColor themeColor0 = {
|
|
{0.25, 0.25, 0.25}, // master
|
|
{0.12, 0.12, 0.12}, // thumb
|
|
{0.6, 0.2, 0.2}, // secttitle
|
|
{0.1, 0.1, 0.1}, // secttitle2
|
|
{0.45, 0.17, 0.17}, // secttitlecheck
|
|
{0.45, 0.17, 0.17}, // secttitlecheck2
|
|
{1.0, 1.0, 1.0}, // sectfont
|
|
{0.7, 0.7, 0.7}, // sectsymbol
|
|
{0.1, 0.1, 0.1}, // sectpane
|
|
{0.25, 0.25, 0.25}, // separator
|
|
{0.1, 0.1, 0.1}, // separator2
|
|
{0.0, 0.0, 1.0}, // shortcut
|
|
{1.0, 1.0, 1.0}, // fontactive
|
|
{0.5, 0.5, 0.5}, // fontinactive
|
|
{0.3, 0.3, 0.3}, // decorinactive
|
|
{0.4, 0.4, 0.4}, // decorinactive2
|
|
{0.6, 0.4, 0.4}, // button
|
|
{0.4, 0.4, 0.7}, // check
|
|
{0.4, 0.6, 0.4}, // radio
|
|
{0.4, 0.6, 0.6}, // select
|
|
{0.2, 0.3, 0.3}, // select2
|
|
{0.3, 0.2, 0.3}, // slider
|
|
{0.6, 0.4, 0.6}, // slider2
|
|
{0.6, 0.6, 0.4}, // edit
|
|
{0.7, 0.0, 0.0}, // edit2
|
|
{0.9, 0.9, 0.9} // cursor
|
|
};
|
|
|
|
|
|
// theme color 1 : orange
|
|
static const mjuiThemeColor themeColor1 = {
|
|
{0.2, 0.2, 0.2}, // master
|
|
{0.12, 0.12, 0.12}, // thumb
|
|
{0.3, 0.3, 0.3}, // secttitle
|
|
{0.15, 0.15, 0.15}, // secttitle2
|
|
{0.25, 0.25, 0.25}, // secttitlecheck
|
|
{0.25, 0.25, 0.25}, // secttitlecheck2
|
|
{0.8, 0.8, 0.8}, // sectfont
|
|
{0.7, 0.7, 0.7}, // sectsymbol
|
|
{0.15, 0.15, 0.15}, // sectpane
|
|
{0.2, 0.2, 0.2}, // separator
|
|
{0.15, 0.15, 0.15}, // separator2
|
|
{0.0, 0.0, 1.0}, // shortcut
|
|
{0.9, 0.9, 0.9}, // fontactive
|
|
{0.5, 0.5, 0.5}, // fontinactive
|
|
{0.2, 0.2, 0.2}, // decorinactive
|
|
{0.25, 0.25, 0.25}, // decorinactive2
|
|
{0.6, 0.4, 0.2}, // button
|
|
{0.6, 0.4, 0.2}, // check
|
|
{0.6, 0.4, 0.2}, // radio
|
|
{0.6, 0.4, 0.2}, // select
|
|
{0.3, 0.2, 0.1}, // select2
|
|
{0.2, 0.2, 0.2}, // slider
|
|
{0.6, 0.4, 0.2}, // slider2
|
|
{0.6, 0.4, 0.2}, // edit
|
|
{0.7, 0.0, 0.0}, // edit2
|
|
{0.9, 0.9, 0.9} // cursor
|
|
};
|
|
|
|
|
|
// theme color 2 : white
|
|
static const mjuiThemeColor themeColor2 = {
|
|
{0.9, 0.9, 0.9}, // master
|
|
{0.7, 0.7, 0.7}, // thumb
|
|
{0.8, 0.8, 0.8}, // secttitle
|
|
{1.0, 1.0, 1.0}, // secttitle2
|
|
{0.95, 0.95, 0.95}, // secttitlecheck
|
|
{0.95, 0.95, 0.95}, // secttitlecheck2
|
|
{0.0, 0.0, 0.8}, // sectfont
|
|
{0.0, 0.0, 0.8}, // sectsymbol
|
|
{1.0, 1.0, 1.0}, // sectpane
|
|
{0.9, 0.9, 0.9}, // separator
|
|
{1.0, 1.0, 1.0}, // separator2
|
|
{0.0, 1.0, 1.0}, // shortcut
|
|
{0.0, 0.0, 0.0}, // fontactive
|
|
{0.7, 0.7, 0.7}, // fontinactive
|
|
{0.95, 0.95, 0.95}, // decorinactive
|
|
{0.9, 0.9, 0.9}, // decorinactive2
|
|
{0.8, 0.8, 0.8}, // button
|
|
{0.8, 0.8, 0.8}, // check
|
|
{0.8, 0.8, 0.8}, // radio
|
|
{0.8, 0.8, 0.8}, // select
|
|
{0.9, 0.9, 0.9}, // select2
|
|
{0.95, 0.95, 0.95}, // slider
|
|
{0.8, 0.8, 0.8}, // slider2
|
|
{0.8, 0.8, 0.8}, // edit
|
|
{1.0, 0.3, 0.3}, // edit2
|
|
{0.2, 0.2, 0.2} // cursor
|
|
};
|
|
|
|
|
|
// theme color 3 : black
|
|
static const mjuiThemeColor themeColor3 = {
|
|
{0.15, 0.15, 0.15}, // master
|
|
{0.3, 0.3, 0.3}, // thumb
|
|
{0.25, 0.25, 0.25}, // secttitle
|
|
{0.0, 0.0, 0.0}, // secttitle2
|
|
{0.2, 0.2, 0.2}, // secttitlecheck
|
|
{0.2, 0.2, 0.2}, // secttitlecheck2
|
|
{1.0, 0.3, 0.3}, // sectfont
|
|
{1.0, 0.3, 0.3}, // sectsymbol
|
|
{0.0, 0.0, 0.0}, // sectpane
|
|
{0.15, 0.15, 0.15}, // separator
|
|
{0.0, 0.0, 0.0}, // separator2
|
|
{0.0, 0.0, 1.0}, // shortcut
|
|
{1.0, 1.0, 1.0}, // fontactive
|
|
{0.4, 0.4, 0.4}, // fontinactive
|
|
{0.1, 0.1, 0.1}, // decorinactive
|
|
{0.15, 0.15, 0.15}, // decorinactive2
|
|
{0.3, 0.3, 0.3}, // button
|
|
{0.3, 0.3, 0.3}, // check
|
|
{0.3, 0.3, 0.3}, // radio
|
|
{0.3, 0.3, 0.3}, // select
|
|
{0.15, 0.15, 0.15}, // select2
|
|
{0.15, 0.15, 0.15}, // slider
|
|
{0.3, 0.3, 0.3}, // slider2
|
|
{0.3, 0.3, 0.3}, // edit
|
|
{0.8, 0.2, 0.2}, // edit2
|
|
{0.8, 0.8, 0.8} // cursor
|
|
};
|
|
|
|
|
|
|
|
//------------------------------------ Utility functions -------------------------------------------
|
|
|
|
// scale from abstract pixels to framebuffer units
|
|
static int SCL(int sz, const mjrContext* con) {
|
|
return mjMAX(0, mju_round(sz * 0.01 * con->fontScale));
|
|
}
|
|
|
|
|
|
|
|
// init OpenGL
|
|
static void initOpenGL(const mjrRect* r, const mjrContext* con) {
|
|
// set OpenGL options
|
|
glDisable(GL_NORMALIZE);
|
|
glDisable(GL_DEPTH_TEST);
|
|
glDisable(GL_CULL_FACE);
|
|
glDisable(GL_LIGHTING);
|
|
glDisable(GL_COLOR_MATERIAL);
|
|
glDisable(GL_BLEND);
|
|
glShadeModel(GL_SMOOTH);
|
|
glPolygonMode(GL_FRONT_AND_BACK, GL_FILL);
|
|
|
|
// standard 2D projection, in framebuffer units
|
|
glMatrixMode(GL_PROJECTION);
|
|
glLoadIdentity();
|
|
glOrtho(0, r->width, 0, r->height, -1, 1);
|
|
glMatrixMode(GL_MODELVIEW);
|
|
glLoadIdentity();
|
|
|
|
// set viewport
|
|
glViewport(r->left, r->bottom, r->width, r->height);
|
|
}
|
|
|
|
|
|
|
|
// get text width up to specificed limit (0: 0, -1: entire string)
|
|
static int textwidth(const char* text, const mjrContext* con, int limit) {
|
|
int i = 0, width = 0;
|
|
|
|
// zero limit
|
|
if (limit == 0) {
|
|
return 0;
|
|
}
|
|
|
|
// add character widths
|
|
while (text[i] && (limit < 1 || i < limit)) {
|
|
width += con->charWidth[(unsigned char)text[i++]];
|
|
}
|
|
|
|
return width;
|
|
}
|
|
|
|
|
|
|
|
// draw text, limit to maxwidth
|
|
static void drawtext(const char* txt, int x, int y, int maxwidth,
|
|
const float* rgb, const mjrContext* con) {
|
|
// determine string length that fits in maxwidth
|
|
int len = 0, width = 0;
|
|
while (txt[len]) {
|
|
width += con->charWidth[(unsigned char)txt[len]];
|
|
if (width >= maxwidth) {
|
|
break;
|
|
} else {
|
|
len++;
|
|
}
|
|
}
|
|
|
|
// draw normal text
|
|
glListBase(con->baseFontNormal);
|
|
glColor3fv(rgb);
|
|
glRasterPos3i(x, y, 0);
|
|
glCallLists(len, GL_UNSIGNED_BYTE, txt);
|
|
}
|
|
|
|
|
|
|
|
// draw text centered in rectangle
|
|
static void drawtextrect(mjrRect rect, const char* txt,
|
|
const float* rgb, const mjrContext* con) {
|
|
int dy = (rect.height - con->charHeight)/2;
|
|
int dx = (rect.width - textwidth(txt, con, -1))/2;
|
|
dx = mjMAX(0, dx);
|
|
|
|
drawtext(txt, rect.left+dx, rect.bottom+dy, rect.width-dx, rgb, con);
|
|
}
|
|
|
|
|
|
|
|
// draw rectangle
|
|
static void drawrectangle(mjrRect rect, const float* rgb, const float* rgbback,
|
|
const mjrContext* con) {
|
|
// outside
|
|
glColor3fv(rgb);
|
|
glBegin(GL_QUADS);
|
|
glVertex2i(rect.left, rect.bottom);
|
|
glVertex2i(rect.left+rect.width, rect.bottom);
|
|
glVertex2i(rect.left+rect.width, rect.bottom+rect.height);
|
|
glVertex2i(rect.left, rect.bottom+rect.height);
|
|
glEnd();
|
|
|
|
// inside
|
|
if (rgbback) {
|
|
int margin = SCL(2, con);
|
|
glColor3fv(rgbback);
|
|
glBegin(GL_QUADS);
|
|
glVertex2i(rect.left+margin, rect.bottom+margin);
|
|
glVertex2i(rect.left+rect.width-margin, rect.bottom+margin);
|
|
glVertex2i(rect.left+rect.width-margin, rect.bottom+rect.height-margin);
|
|
glVertex2i(rect.left+margin, rect.bottom+rect.height-margin);
|
|
glEnd();
|
|
}
|
|
}
|
|
|
|
|
|
|
|
// round corners of rectangle
|
|
static void roundcorner(mjrRect rect, int flg_skipbottom, int flg_separator,
|
|
const mjUI* ui, const mjrContext* con) {
|
|
// get rounding from theme, exit if disabled
|
|
int cornerspec = flg_separator ? ui->spacing.cornersep : ui->spacing.cornersect;
|
|
if (cornerspec == 0) {
|
|
return;
|
|
}
|
|
|
|
// quarter-circle divisions and radius
|
|
int ndivide = 10;
|
|
double radius = cornerspec * 0.01 * con->fontScale;
|
|
|
|
// draw fans in the four corners, optionally skip bottom corners
|
|
for (int ic = (flg_skipbottom ? 2 : 0); ic < 4; ++ic) {
|
|
// set corner
|
|
double corner[2];
|
|
switch (ic) {
|
|
case 0: // bottom-left
|
|
corner[0] = rect.left;
|
|
corner[1] = rect.bottom;
|
|
break;
|
|
|
|
case 1: // bottom-right
|
|
corner[0] = rect.left + rect.width;
|
|
corner[1] = rect.bottom;
|
|
break;
|
|
|
|
case 2: // top-right
|
|
corner[0] = rect.left + rect.width;
|
|
corner[1] = rect.bottom + rect.height;
|
|
break;
|
|
|
|
default: // top-left
|
|
corner[0] = rect.left;
|
|
corner[1] = rect.bottom + rect.height;
|
|
}
|
|
|
|
// orient fan to point inside
|
|
double angle = ic * 0.5 * mjPI;
|
|
|
|
// compute circle center: opposite to corner
|
|
double center[2];
|
|
center[0] = corner[0] + mju_sqrt(2.0) * radius * cos(angle + 0.25 * mjPI);
|
|
center[1] = corner[1] + mju_sqrt(2.0) * radius * sin(angle + 0.25 * mjPI);
|
|
|
|
// fill with erase color, start trinagle_fan from corner
|
|
glColor3fv(flg_separator ? ui->color.sectpane : ui->color.master);
|
|
glBegin(GL_TRIANGLE_FAN);
|
|
glVertex2d(corner[0], corner[1]);
|
|
|
|
// compute vertices of quarter-circle
|
|
for (int i = 0; i <= ndivide; i++) {
|
|
double a = angle + mjPI + 0.5 * mjPI * (double)i / (double)ndivide;
|
|
glVertex2d(center[0] + radius * cos(a), center[1] + radius * sin(a));
|
|
}
|
|
glEnd();
|
|
}
|
|
}
|
|
|
|
|
|
|
|
// draw oval
|
|
static void drawoval(mjrRect rect, const float* rgb, const float* rgbback,
|
|
const mjrContext* con) {
|
|
const int ndivide = 15;
|
|
|
|
// require horizontal
|
|
if (rect.height > rect.width) {
|
|
return;
|
|
}
|
|
|
|
// circle info
|
|
double radius = 0.5*rect.height;
|
|
double lcenter[2] = {rect.left + radius, rect.bottom + radius};
|
|
double rcenter[2] = {rect.left + rect.width - radius, rect.bottom + radius};
|
|
|
|
// filled
|
|
glColor3fv(rgb);
|
|
glBegin(GL_POLYGON);
|
|
|
|
// draw left half-circle
|
|
for (int i=0; i <= ndivide; i++) {
|
|
double angle = mjPI * (0.5 + (double)i / (double)ndivide);
|
|
glVertex2d(lcenter[0] + radius*cos(angle), lcenter[1] + radius*sin(angle));
|
|
}
|
|
|
|
// draw right half-circle
|
|
for (int i=0; i <= ndivide; i++) {
|
|
double angle = mjPI * (1.5 + (double)i / (double)ndivide);
|
|
glVertex2d(rcenter[0] + radius*cos(angle), rcenter[1] + radius*sin(angle));
|
|
}
|
|
glEnd();
|
|
|
|
// inside
|
|
if (rgbback) {
|
|
int margin = SCL(2, con);
|
|
radius -= margin;
|
|
|
|
glColor3fv(rgbback);
|
|
glBegin(GL_POLYGON);
|
|
|
|
// draw left half-circle
|
|
for (int i=0; i <= ndivide; i++) {
|
|
double angle = mjPI * (0.5 + (double)i / (double)ndivide);
|
|
glVertex2d(lcenter[0] + radius*cos(angle), lcenter[1] + radius*sin(angle));
|
|
}
|
|
|
|
// draw right half-circle
|
|
for (int i=0; i <= ndivide; i++) {
|
|
double angle = mjPI * (1.5 + (double)i / (double)ndivide);
|
|
glVertex2d(rcenter[0] + radius*cos(angle), rcenter[1] + radius*sin(angle));
|
|
}
|
|
glEnd();
|
|
}
|
|
}
|
|
|
|
|
|
|
|
// draw open/closed symbol in title
|
|
// type: 0- section, 1- section with checkbox, 2- separator
|
|
static void drawsymbol(mjrRect rect, int flg_open, int type,
|
|
const mjUI* ui, const mjrContext* con) {
|
|
// size and center
|
|
int texthor = SCL(ui->spacing.texthor, con);
|
|
int cx = rect.left + rect.width - texthor;
|
|
int cy = rect.bottom + rect.height/2;
|
|
int d = mju_round(con->charHeight*0.33);
|
|
|
|
// separator size
|
|
if (type == 2) {
|
|
d = mju_round(con->charHeight*0.28);
|
|
}
|
|
|
|
// open
|
|
if (flg_open) {
|
|
glColor3fv(ui->color.sectsymbol);
|
|
glBegin(GL_TRIANGLES);
|
|
glVertex2i(cx, cy+d);
|
|
glVertex2i(cx-2*d, cy+d);
|
|
glVertex2i(cx-d, cy-d);
|
|
glEnd();
|
|
}
|
|
|
|
// closed
|
|
else {
|
|
// solid outside
|
|
glColor3fv(ui->color.sectsymbol);
|
|
glBegin(GL_TRIANGLES);
|
|
glVertex2i(cx, cy-d);
|
|
glVertex2i(cx, cy+d);
|
|
glVertex2i(cx-2*d, cy);
|
|
glEnd();
|
|
|
|
// set color for inside
|
|
switch (type) {
|
|
case 0: // section
|
|
glColor3f(
|
|
(ui->color.secttitle[0] + ui->color.secttitle2[0]) * 0.5,
|
|
(ui->color.secttitle[1] + ui->color.secttitle2[1]) * 0.5,
|
|
(ui->color.secttitle[2] + ui->color.secttitle2[2]) * 0.5
|
|
);
|
|
break;
|
|
|
|
case 1: // section with checkbox
|
|
glColor3f(
|
|
(ui->color.secttitlecheck[0] + ui->color.secttitlecheck2[0]) * 0.5,
|
|
(ui->color.secttitlecheck[1] + ui->color.secttitlecheck2[1]) * 0.5,
|
|
(ui->color.secttitlecheck[2] + ui->color.secttitlecheck2[2]) * 0.5
|
|
);
|
|
break;
|
|
|
|
case 2: // separator
|
|
glColor3f(
|
|
(ui->color.separator[0] + ui->color.separator2[0]) * 0.5,
|
|
(ui->color.separator[1] + ui->color.separator2[1]) * 0.5,
|
|
(ui->color.separator[2] + ui->color.separator2[2]) * 0.5
|
|
);
|
|
}
|
|
|
|
// draw inside
|
|
double margin = con->fontScale * 0.015;
|
|
double u = 0.5 * sqrt(5.0) * margin;
|
|
double y = d - u - 0.5 * margin;
|
|
glBegin(GL_TRIANGLES);
|
|
glVertex2d(cx-margin, cy-y);
|
|
glVertex2d(cx-margin, cy+y);
|
|
glVertex2d(cx-2*d+2*u, cy);
|
|
glEnd();
|
|
}
|
|
}
|
|
|
|
|
|
|
|
// radio element rectangle
|
|
static mjrRect radioelement(const mjuiItem* it, int n,
|
|
const mjUI* ui, const mjrContext* con) {
|
|
// scale sizes from theme
|
|
int g_itemmid = SCL(ui->spacing.itemmid, con);
|
|
int g_textver = SCL(ui->spacing.textver, con);
|
|
int ncol = ui->radiocol ? ui->radiocol : 2;
|
|
int nrow = (it->multi.nelem-1)/ncol + 1;
|
|
|
|
// compute elements
|
|
int cellwidth = (it->rect.width-(ncol-1)*g_itemmid)/ncol;
|
|
int cellheight = con->charHeight+2*g_textver;
|
|
int row = n/ncol;
|
|
int col = n%ncol;
|
|
|
|
// construct rectangle
|
|
mjrRect r = {
|
|
it->rect.left + col*(cellwidth+g_itemmid),
|
|
it->rect.bottom+(nrow-1-row)*cellheight,
|
|
cellwidth,
|
|
cellheight
|
|
};
|
|
|
|
// adjust width of last column
|
|
if (col == ncol-1) {
|
|
r.width = it->rect.width - r.left + it->rect.left;
|
|
}
|
|
|
|
return r;
|
|
}
|
|
|
|
|
|
|
|
// compute mouse position relative to ui
|
|
static void mouseinui(const mjUI* ui, const mjuiState* ins, int* x, int* y) {
|
|
// extract data
|
|
*x = (int)ins->x;
|
|
*y = (int)ins->y;
|
|
mjrRect rect = ins->rect[ui->rectid];
|
|
|
|
// correct for scrollbar
|
|
if (ui->height > rect.height) {
|
|
*y -= ui->scroll;
|
|
}
|
|
|
|
// correct for offset
|
|
*x -= rect.left;
|
|
*y -= (rect.height - ui->height) + rect.bottom;
|
|
}
|
|
|
|
|
|
|
|
// compute mouse position relative to rectangle
|
|
static void mouseinrect(mjrRect rect, const mjUI* ui, const mjuiState* ins,
|
|
double* rx, double* ry) {
|
|
// mouse relative to ui
|
|
int x, y;
|
|
mouseinui(ui, ins, &x, &y);
|
|
|
|
// mouse relative to rectangle
|
|
*rx = (x-rect.left) / (double)rect.width;
|
|
*ry = (y-rect.bottom) / (double)rect.height;
|
|
}
|
|
|
|
|
|
|
|
// find radio element under mouse
|
|
static int findradio(const mjuiItem* it, const mjUI* ui,
|
|
const mjuiState* ins, const mjrContext* con) {
|
|
// number of rows and columns
|
|
int ncol = ui->radiocol ? ui->radiocol : 2;
|
|
int nrow = (it->multi.nelem-1)/ncol + 1;
|
|
|
|
// get mouse relative to rect
|
|
double rx, ry;
|
|
mouseinrect(it->rect, ui, ins, &rx, &ry);
|
|
|
|
// count rows from top
|
|
ry = 1-ry;
|
|
|
|
// round and clamp
|
|
int row = (int)floor(ry*nrow);
|
|
int col = (int)floor(rx*ncol);
|
|
row = mjMAX(0, mjMIN(row, nrow-1));
|
|
col = mjMAX(0, mjMIN(col, ncol-1));
|
|
|
|
// result
|
|
int ind = row*ncol + col;
|
|
if (ind < it->multi.nelem) {
|
|
return ind;
|
|
} else {
|
|
return -1;
|
|
}
|
|
}
|
|
|
|
|
|
|
|
// make list of separators for radioline elements
|
|
static void makeradioline(const mjuiItem* it, const mjrContext* con, int* sep) {
|
|
int nelem = it->multi.nelem, totwid = 0;
|
|
int elwid[mjMAXUIMULTI];
|
|
|
|
// no elements
|
|
if (!nelem) {
|
|
return;
|
|
}
|
|
|
|
// compute element widths
|
|
for (int i=0; i < nelem; i++) {
|
|
elwid[i] = textwidth(it->multi.name[i], con, -1);
|
|
totwid += elwid[i];
|
|
}
|
|
|
|
// compute per-element extra space
|
|
double extra = ((double)(it->rect.width - totwid)) / ((double)nelem);
|
|
|
|
// compute separators
|
|
sep[0] = 0;
|
|
for (int i=0; i < nelem; i++) {
|
|
sep[i+1] = sep[i] + elwid[i] + mju_round((i+1)*extra) -mju_round(i*extra);
|
|
}
|
|
sep[nelem] = it->rect.width;
|
|
}
|
|
|
|
|
|
|
|
// find radioline element under mouse
|
|
static int findradioline(const mjuiItem* it, const mjUI* ui,
|
|
const mjuiState* ins, const mjrContext* con) {
|
|
// make separators
|
|
int sep[mjMAXUIMULTI+1];
|
|
makeradioline(it, con, sep);
|
|
|
|
// get mouse relative to rect
|
|
double rx, ry;
|
|
mouseinrect(it->rect, ui, ins, &rx, &ry);
|
|
|
|
// find interval
|
|
int x = mju_round(rx*it->rect.width);
|
|
for (int i=0; i < it->multi.nelem; i++) {
|
|
if (x >= sep[i] && x < sep[i+1]) {
|
|
return i;
|
|
}
|
|
}
|
|
|
|
// not found
|
|
return -1;
|
|
}
|
|
|
|
|
|
|
|
// find select element under mouse
|
|
static int findselect(const mjuiItem* it, const mjUI* ui,
|
|
const mjuiState* ins, const mjrContext* con) {
|
|
// not tracking: nothing to do
|
|
if (!(ui->mousesect > 0 && ui->mouseitem >= 0 && it && it->type == mjITEM_SELECT)) {
|
|
return -1;
|
|
}
|
|
|
|
// make box
|
|
int g_textver = SCL(ui->spacing.textver, con);
|
|
int cellheight = con->charHeight + 2*g_textver;
|
|
mjrRect r = it->rect;
|
|
r.height = it->multi.nelem * cellheight;
|
|
r.bottom -= r.height;
|
|
|
|
// find in box
|
|
double rx, ry;
|
|
mouseinrect(r, ui, ins, &rx, &ry);
|
|
if (ry > 0 && ry < 1 && rx > 0 && rx < 1) {
|
|
int k = (int)floor(ry*it->multi.nelem);
|
|
k = mjMAX(0, mjMIN(it->multi.nelem-1, k));
|
|
return it->multi.nelem-1-k;
|
|
} else {
|
|
return -1;
|
|
}
|
|
}
|
|
|
|
|
|
|
|
// make scrollbar rectangles
|
|
void scrollrect(mjrRect rect, const mjUI* ui, const mjrContext* con,
|
|
mjrRect* bar, mjrRect* thumb) {
|
|
int w_scroll = SCL(ui->spacing.scroll, con);
|
|
|
|
// bar
|
|
*bar = rect;
|
|
bar->left = rect.left+rect.width-w_scroll;
|
|
bar->width = w_scroll;
|
|
|
|
// thumb
|
|
double tstart = (double)ui->scroll / (double)ui->height;
|
|
double tend = (double)(ui->scroll+rect.height) / (double)ui->height;
|
|
*thumb = *bar;
|
|
thumb->bottom = rect.bottom + mju_round(rect.height*(1-tend));
|
|
thumb->height = mju_round(rect.height*(tend-tstart));
|
|
}
|
|
|
|
|
|
|
|
// is point in rectangle
|
|
static int inside(int x, int y, mjrRect r) {
|
|
return (x >= r.left && x <= r.left+r.width && y >= r.bottom && y <= r.bottom+r.height);
|
|
}
|
|
|
|
|
|
|
|
// is point in oval
|
|
static int insideoval(int x, int y, mjrRect r) {
|
|
// exclude if not in rectangle
|
|
if (!inside(x, y, r)) {
|
|
return 0;
|
|
}
|
|
|
|
// check center
|
|
int radius = r.height/2;
|
|
if (x >= r.left+radius && x <= r.left+r.width-radius) {
|
|
return 1;
|
|
}
|
|
|
|
// left circle
|
|
int dx = x - (r.left+radius);
|
|
int dy = y - (r.bottom+radius);
|
|
if (dx < 0 && (dx*dx + dy*dy < radius*radius)) {
|
|
return 1;
|
|
}
|
|
|
|
// right circle
|
|
dx = x - (r.left+r.width-radius);
|
|
if (dx > 0 && (dx*dx + dy*dy < radius*radius)) {
|
|
return 1;
|
|
}
|
|
|
|
return 0;
|
|
}
|
|
|
|
|
|
|
|
// find mouse location in UI; y already inverted
|
|
// sect: -1: thumb, -2: slider down, -3: slider up, positive: 1+section
|
|
// item: -1: section title or scroll, non-negative: item number
|
|
// item: -2 in checkbox on section title
|
|
static void findmouse(const mjUI* ui, const mjuiState* ins, const mjrContext* con,
|
|
int* sect, int* item) {
|
|
// clear
|
|
*sect = 0;
|
|
*item = 0;
|
|
|
|
// extract data
|
|
int x = (int)ins->x;
|
|
int y = (int)ins->y;
|
|
mjrRect rect = ins->rect[ui->rectid];
|
|
|
|
// scrollbar
|
|
if (ui->height > rect.height) {
|
|
// construct rectangles
|
|
mjrRect bar;
|
|
mjrRect thumb;
|
|
scrollrect(rect, ui, con, &bar, &thumb);
|
|
|
|
// inside bar
|
|
if (inside(x, y, bar)) {
|
|
// inside thumb
|
|
if (inside(x, y, thumb)) {
|
|
*sect = -1;
|
|
}
|
|
|
|
// below thumb
|
|
else if (y < thumb.bottom) {
|
|
*sect = -2;
|
|
}
|
|
|
|
// above thumb
|
|
else {
|
|
*sect = -3;
|
|
}
|
|
|
|
// done
|
|
*item = -1;
|
|
return;
|
|
}
|
|
}
|
|
|
|
// adjust x,y
|
|
mouseinui(ui, ins, &x, &y);
|
|
|
|
// check sections and items
|
|
for (int n=0; n < ui->nsect; n++) {
|
|
// get section pointer
|
|
const mjuiSection* s = ui->sect + n;
|
|
|
|
// in title
|
|
if (s->state < 2 && inside(x, y, s->rtitle)) {
|
|
*sect = n+1;
|
|
*item = -1;
|
|
|
|
// in checkbox
|
|
if (s->checkbox > 0) {
|
|
mjrRect rcheck = s->rtitle;
|
|
rcheck.width = mjMIN(rcheck.height, rcheck.width);
|
|
if (inside(x, y, rcheck)) {
|
|
*item = -2;
|
|
}
|
|
}
|
|
|
|
return;
|
|
}
|
|
|
|
// in content and open: check items
|
|
if (s->state && inside(x, y, s->rcontent)) {
|
|
for (int i=0; i < s->nitem; i++) {
|
|
if (s->item[i].type == mjITEM_BUTTON ?
|
|
insideoval(x, y, s->item[i].rect) :
|
|
inside(x, y, s->item[i].rect)) {
|
|
*sect = n+1;
|
|
*item = i;
|
|
return;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
|
|
|
|
// set slider position given mouse
|
|
static void setslider(mjuiItem* it, mjUI* ui,
|
|
const mjuiState* ins, const mjrContext* con) {
|
|
// get mouse relative position
|
|
double rx, ry;
|
|
mouseinrect(it->rect, ui, ins, &rx, &ry);
|
|
|
|
// clamp rx, enforce divisions
|
|
rx = mjMAX(0, mjMIN(1, rx));
|
|
rx = mju_round(rx * it->slider.divisions) / mjMAX(1, it->slider.divisions);
|
|
rx = mjMAX(0, mjMIN(1, rx));
|
|
|
|
// compute value
|
|
mjtNum val = (mjtNum)(it->slider.range[0]*(1-rx) + it->slider.range[1]*rx);
|
|
|
|
// set slider position
|
|
if (it->type == mjITEM_SLIDERINT) {
|
|
*(int*)it->pdata = mju_round(val);
|
|
} else {
|
|
*(mjtNum*)it->pdata = val;
|
|
}
|
|
}
|
|
|
|
|
|
|
|
// check edit text before conversion to array
|
|
// return 0 if ok, error code otherwise
|
|
static int checkedit(const char* text, const mjuiItem* it) {
|
|
// text always passes
|
|
if (it->type == mjITEM_EDITTXT) {
|
|
return 0;
|
|
}
|
|
|
|
// check type
|
|
if (it->type != mjITEM_EDITINT &&
|
|
it->type != mjITEM_EDITNUM &&
|
|
it->type != mjITEM_EDITFLOAT) {
|
|
mju_error("Internal error: expected edit control");
|
|
}
|
|
|
|
// convert
|
|
double val[mjMAXUIEDIT];
|
|
int n = sscanf(text, "%lf %lf %lf %lf %lf %lf %lf",
|
|
val, val+1, val+2, val+3, val+4, val+5, val+6);
|
|
|
|
// check length
|
|
if (n != it->edit.nelem) {
|
|
return 1;
|
|
}
|
|
|
|
// check range when defined
|
|
for (int i=0; i < n; i++)
|
|
if (it->edit.range[i][0] < it->edit.range[i][1] &&
|
|
(val[i] < it->edit.range[i][0] || val[i] > it->edit.range[i][1])) {
|
|
return 2;
|
|
}
|
|
|
|
// require int values for int type
|
|
if (it->type == mjITEM_EDITINT) {
|
|
for (int i=0; i < n; i++) {
|
|
if (val[i] != (double)((int)val[i])) {
|
|
return 3;
|
|
}
|
|
}
|
|
}
|
|
|
|
return 0;
|
|
}
|
|
|
|
|
|
|
|
// convert edit text to numeric array
|
|
// return 0 if ok, error code otherwise
|
|
static int text2array(const char* text, const mjuiItem* it) {
|
|
// text: copy
|
|
if (it->type == mjITEM_EDITTXT) {
|
|
// copy string, assume mjMAXUINAME allocation
|
|
char* pdata = (char*)it->pdata;
|
|
strncpy(pdata, text, mjMAXUINAME);
|
|
pdata[mjMAXUINAME-1] = 0;
|
|
return 0;
|
|
}
|
|
|
|
// check type
|
|
if (it->type != mjITEM_EDITINT && it->type != mjITEM_EDITNUM && it->type != mjITEM_EDITFLOAT) {
|
|
mju_error("Internal error: expected edit control");
|
|
}
|
|
|
|
// convert
|
|
double val[mjMAXUIEDIT];
|
|
int n = sscanf(text, "%lf %lf %lf %lf %lf %lf %lf",
|
|
val, val+1, val+2, val+3, val+4, val+5, val+6);
|
|
|
|
// check length
|
|
if (n != it->edit.nelem) {
|
|
return 1;
|
|
}
|
|
|
|
// check range when defined
|
|
for (int i=0; i < n; i++) {
|
|
if (it->edit.range[i][0] < it->edit.range[i][1] &&
|
|
(val[i] < it->edit.range[i][0] || val[i] > it->edit.range[i][1])) {
|
|
return 2;
|
|
}
|
|
}
|
|
|
|
// require int values for int type
|
|
if (it->type == mjITEM_EDITINT) {
|
|
for (int i=0; i < n; i++) {
|
|
if (val[i] != (double)((int)val[i])) {
|
|
return 3;
|
|
}
|
|
}
|
|
}
|
|
|
|
// copy values
|
|
if (it->type == mjITEM_EDITINT) {
|
|
int* pdata = (int*)it->pdata;
|
|
for (int i=0; i < n; i++) {
|
|
pdata[i] = (int)val[i];
|
|
}
|
|
} else {
|
|
if (it->type == mjITEM_EDITNUM) {
|
|
mjtNum* pdata = (mjtNum*)it->pdata;
|
|
for (int i=0; i < n; i++) {
|
|
pdata[i] = (mjtNum)val[i];
|
|
}
|
|
} else {
|
|
float* pdata = (float*)it->pdata;
|
|
for (int i=0; i < n; i++) {
|
|
pdata[i] = (float)val[i];
|
|
}
|
|
}
|
|
}
|
|
|
|
return 0;
|
|
}
|
|
|
|
|
|
|
|
// convert numeric array to edit text
|
|
static void array2text(char* text, const mjuiItem* it) {
|
|
// text: copy
|
|
if (it->type == mjITEM_EDITTXT) {
|
|
// copy string, assume mjMAXUINAME allocation
|
|
strncpy(text, (const char*)it->pdata, mjMAXUINAME);
|
|
text[mjMAXUINAME-1] = 0;
|
|
return;
|
|
}
|
|
|
|
// check type
|
|
if (it->type != mjITEM_EDITINT && it->type != mjITEM_EDITNUM && it->type != mjITEM_EDITFLOAT) {
|
|
mju_error("Internal error: expected edit control");
|
|
}
|
|
|
|
// print
|
|
int n = it->edit.nelem;
|
|
char buf[50];
|
|
text[0] = 0;
|
|
for (int i=0; i < n; i++) {
|
|
if (it->type == mjITEM_EDITINT) {
|
|
mjSNPRINTF(buf, "%d", ((int*)it->pdata)[i]);
|
|
} else if (it->type == mjITEM_EDITNUM) {
|
|
mjSNPRINTF(buf, "%.4g", ((mjtNum*)it->pdata)[i]);
|
|
} else {
|
|
mjSNPRINTF(buf, "%.4g", ((float*)it->pdata)[i]);
|
|
}
|
|
strncat(text, buf, mjMAXUITEXT - strlen(text) - 1);
|
|
if (i < n-1) {
|
|
strncat(text, " ", mjMAXUITEXT - strlen(text) - 1);
|
|
}
|
|
}
|
|
}
|
|
|
|
|
|
// return (remapped) key if valid, 0 if not valid
|
|
static int validkey(int key, int sz, int type, const mjuiState* state) {
|
|
// text
|
|
if (type == mjITEM_EDITTXT) {
|
|
if (sz < mjMAXUINAME-1 && key >= 32 && key <= 127) {
|
|
// lower case if Shift (Caps Lock cannot be detected in GLWF)
|
|
if (key >= 'A' && key <= 'Z' && !state->shift) {
|
|
key = key + 'a' - 'A';
|
|
}
|
|
|
|
// Shift: remap remaining keys
|
|
else if (state->shift) {
|
|
if (key == '`') {
|
|
key = '~';
|
|
} else if (key == '-') {
|
|
key = '_';
|
|
} else if (key == '=') {
|
|
key = '+';
|
|
} else if (key == '[') {
|
|
key = '{';
|
|
} else if (key == ']') {
|
|
key = '}';
|
|
} else if (key == '\\') {
|
|
key = '|';
|
|
} else if (key == ';') {
|
|
key = ':';
|
|
} else if (key == '\'') {
|
|
key = '"';
|
|
} else if (key == ',') {
|
|
key = '<';
|
|
} else if (key == '.') {
|
|
key = '>';
|
|
} else if (key == '/') {
|
|
key = '?';
|
|
}
|
|
}
|
|
|
|
return key;
|
|
} else {
|
|
return 0;
|
|
}
|
|
}
|
|
|
|
// numeric
|
|
else if (type == mjITEM_EDITINT || type == mjITEM_EDITNUM || type == mjITEM_EDITFLOAT) {
|
|
if (sz < (mjMAXUITEXT-1) &&
|
|
(key == ' ' || key == '+' || key == '=' || key == '-' || (key >= '0' && key <= '9') ||
|
|
(key >= mjKEY_NUMPAD_0 && key <= mjKEY_NUMPAD_9) ||
|
|
((key == 'e' || key == 'E' || key == '.') &&
|
|
(type == mjITEM_EDITNUM || type == mjITEM_EDITFLOAT)))) {
|
|
|
|
// remap '=' to '+'
|
|
if (key == '=') {
|
|
key = '+';
|
|
}
|
|
|
|
// remap 'E' to 'e'
|
|
if (key == 'E') {
|
|
key = 'e';
|
|
}
|
|
|
|
// remap numberpad to top row
|
|
if (key >= mjKEY_NUMPAD_0 && key <= mjKEY_NUMPAD_9) {
|
|
key = key - mjKEY_NUMPAD_0 + '0';
|
|
}
|
|
|
|
return key;
|
|
|
|
} else {
|
|
return 0;
|
|
}
|
|
}
|
|
|
|
// other
|
|
else {
|
|
return 0;
|
|
}
|
|
}
|
|
|
|
|
|
|
|
// adjust editscroll so cursor is visible
|
|
static void revealcursor(mjrRect r, mjUI* ui, const mjrContext* con) {
|
|
// scroll left
|
|
if (ui->editcursor <= ui->editscroll) {
|
|
ui->editscroll = ui->editcursor;
|
|
return;
|
|
}
|
|
|
|
// width of available text area
|
|
int width = r.width - 2*SCL(ui->spacing.texthor, con);
|
|
|
|
// scan backwards
|
|
int i = ui->editcursor;
|
|
while (width >= 0 && i >= ui->editscroll) {
|
|
i--;
|
|
width -= con->charWidth[(unsigned char)ui->edittext[i]];
|
|
}
|
|
|
|
// adjust scroll if out of width
|
|
if (width < 0) {
|
|
ui->editscroll = i+1;
|
|
}
|
|
}
|
|
|
|
|
|
|
|
// use mouse position to set editcursor, adjust editscroll
|
|
static void setcursor(mjrRect r, mjUI* ui, const mjuiState* ins, const mjrContext* con) {
|
|
// correct rectangle for texthor
|
|
int g_texthor = SCL(ui->spacing.texthor, con);
|
|
mjrRect r1 = r;
|
|
r1.left += g_texthor;
|
|
r1.width -= 2*g_texthor;
|
|
|
|
// get mouse relative position
|
|
double rx, ry;
|
|
mouseinrect(r1, ui, ins, &rx, &ry);
|
|
|
|
// outside rectangle
|
|
if (rx < 0) {
|
|
ui->editcursor = 0;
|
|
ui->editscroll = 0;
|
|
} else if (rx > 1) {
|
|
ui->editcursor = strlen(ui->edittext);
|
|
revealcursor(r, ui, con);
|
|
}
|
|
|
|
// inside rectangle
|
|
else {
|
|
int besti = ui->editscroll, cumsum = 0;
|
|
int R = mju_round(r1.width*rx), bestdif = R;
|
|
|
|
// find closest between-char position
|
|
for (int i=ui->editscroll; i < strlen(ui->edittext); i++) {
|
|
// add next width
|
|
cumsum += con->charWidth[(unsigned char)ui->edittext[i]];
|
|
|
|
// update best
|
|
if (bestdif > abs(cumsum-R)) {
|
|
bestdif = abs(cumsum-R);
|
|
besti = i+1;
|
|
}
|
|
}
|
|
|
|
// set
|
|
ui->editcursor = besti;
|
|
}
|
|
}
|
|
|
|
|
|
|
|
// parse modifier and shortcut
|
|
static void parseshortcut(const char* text, int* mod, int* key) {
|
|
// clear
|
|
*mod = 0;
|
|
*key = 0;
|
|
|
|
// empty: return
|
|
if (!text[0]) {
|
|
return;
|
|
}
|
|
|
|
// require between 2 and 5 characters
|
|
if (strlen(text) < 2 || strlen(text) > 5) {
|
|
mju_error("mjui_add: invalid shortcut specification");
|
|
}
|
|
|
|
// modifier
|
|
switch (text[0]) {
|
|
case ' ':
|
|
*mod = 0;
|
|
break;
|
|
|
|
case 'C':
|
|
*mod = 1;
|
|
break;
|
|
|
|
case 'S':
|
|
*mod = 2;
|
|
break;
|
|
|
|
case 'A':
|
|
*mod = 4;
|
|
break;
|
|
|
|
default:
|
|
mju_error("mjui_add: invalid shortcut modifier");
|
|
}
|
|
|
|
// key
|
|
if (text[1] == '#') {
|
|
if (sscanf(text+2, "%d", key) != 1) {
|
|
mju_error("mjui_add: invalid shortcut numeric code");
|
|
}
|
|
} else {
|
|
if (text[2] != 0) {
|
|
mju_error("mjui_add: invalid shortcut");
|
|
}
|
|
*key = (int)text[1];
|
|
}
|
|
}
|
|
|
|
|
|
|
|
// check fpr matching modifier and shortcut
|
|
static int matchshortcut(const mjuiState* ins, int mod, int key) {
|
|
// match key
|
|
if (!key || ins->key != key) {
|
|
return 0;
|
|
}
|
|
|
|
// match modifier
|
|
if ((ins->control != 0) + 2*(ins->shift != 0) + 4*(ins->alt != 0) != mod) {
|
|
return 0;
|
|
}
|
|
|
|
return 1;
|
|
}
|
|
|
|
|
|
|
|
//---------------------------------- Public API ----------------------------------------------------
|
|
|
|
// Get builtin UI theme spacing (0-1).
|
|
mjuiThemeSpacing mjui_themeSpacing(int ind) {
|
|
if (ind == 0) {
|
|
return themeSpacing0;
|
|
} else {
|
|
return themeSpacing1;
|
|
}
|
|
}
|
|
|
|
|
|
|
|
// Get builtin UI theme color (0-3).
|
|
mjuiThemeColor mjui_themeColor(int ind) {
|
|
if (ind == 0) {
|
|
return themeColor0;
|
|
} else if (ind == 1) {
|
|
return themeColor1;
|
|
} else if (ind == 2) {
|
|
return themeColor2;
|
|
} else {
|
|
return themeColor3;
|
|
}
|
|
}
|
|
|
|
|
|
|
|
// Add definitions to UI.
|
|
void mjui_add(mjUI* ui, const mjuiDef* def) {
|
|
int n = 0, i, start, num;
|
|
double x[1+2*mjMAXUIEDIT];
|
|
|
|
// process entries until end marker
|
|
while (def[n].type != mjITEM_END) {
|
|
// section
|
|
if (def[n].type == mjITEM_SECTION) {
|
|
// check limit
|
|
if (ui->nsect >= mjMAXUISECT) {
|
|
mju_error("mjui_add: too many sections");
|
|
}
|
|
|
|
// check data
|
|
if (strlen(def[n].name) >= mjMAXUINAME-1) {
|
|
mju_error("mjui_add: section name too long");
|
|
}
|
|
if (def[n].state != mjSECT_CLOSED && def[n].state != mjSECT_OPEN &&
|
|
def[n].state != mjSECT_FIXED && def[n].state != mjPRESERVE) {
|
|
mju_error("mjui_add: invalid section state");
|
|
}
|
|
|
|
// add section, save state
|
|
ui->nsect++;
|
|
mjuiSection* se = ui->sect + (ui->nsect-1);
|
|
int oldstate = se->state;
|
|
|
|
// clear, but preserve item states
|
|
int itemstate[mjMAXUIITEM];
|
|
for (int i = 0; i < mjMAXUIITEM; ++i) {
|
|
itemstate[i] = se->item[i].state;
|
|
}
|
|
memset(se, 0, sizeof(mjuiSection));
|
|
for (int i = 0; i < mjMAXUIITEM; ++i) {
|
|
se->item[i].state = itemstate[i];
|
|
}
|
|
|
|
// set or restore section state
|
|
se->state = (def[n].state == mjPRESERVE ? oldstate : def[n].state);
|
|
|
|
// copy remaining data
|
|
mjSTRNCPY(se->name, def[n].name);
|
|
se->checkbox = def[n].otherint;
|
|
parseshortcut(def[n].other, &(se->modifier), &(se->shortcut));
|
|
}
|
|
|
|
// item
|
|
else if (def[n].type >= 0 && def[n].type < mjNITEM) {
|
|
// first section must be defined
|
|
if (ui->nsect <= 0) {
|
|
mju_error("mjui_add: item defined outside section");
|
|
}
|
|
mjuiSection* se = ui->sect + (ui->nsect-1);
|
|
|
|
// check limit
|
|
if (se->nitem >= mjMAXUIITEM) {
|
|
mju_error("mjui_add: too many items in section");
|
|
}
|
|
|
|
// check item data
|
|
if (def[n].type < 0 || def[n].type >= mjNITEM) {
|
|
mju_error("mjui_add: invalid item type");
|
|
}
|
|
if (strlen(def[n].name) >= mjMAXUINAME) {
|
|
mju_error("mjui_add: item name too long");
|
|
}
|
|
if (def[n].state < 0) {
|
|
mju_error("mjui_add: invalid item state");
|
|
}
|
|
|
|
// add item, save state, clear
|
|
se->nitem++;
|
|
mjuiItem* it = se->item + (se->nitem-1);
|
|
int oldstate = it->state;
|
|
memset(it, 0, sizeof(mjuiItem));
|
|
|
|
// set or restore state for collapsible separator, copy state for others
|
|
if (def[n].type == mjITEM_SEPARATOR && def[n].state == mjPRESERVE) {
|
|
// mjSEPCLOSED makes separator collapsible
|
|
it->state = (oldstate < mjSEPCLOSED ? mjSEPCLOSED : oldstate);
|
|
}
|
|
else {
|
|
it->state = def[n].state;
|
|
}
|
|
|
|
// copy common data
|
|
it->type = def[n].type;
|
|
it->pdata = def[n].pdata;
|
|
mjSTRNCPY(it->name, def[n].name);
|
|
it->sectionid = ui->nsect - 1;
|
|
it->itemid = se->nitem - 1;
|
|
it->userid = def[n].otherint;
|
|
|
|
// data pointer check
|
|
if (it->type > mjITEM_BUTTON && it->pdata == 0) {
|
|
mju_error("mjui_add: no data pointer for item with data");
|
|
}
|
|
if (it->type <= mjITEM_BUTTON && it->pdata) {
|
|
mju_error("mjui_add: data pointer for item without data");
|
|
}
|
|
|
|
// parse button and check
|
|
if (it->type == mjITEM_BUTTON ||
|
|
it->type == mjITEM_CHECKINT ||
|
|
it->type == mjITEM_CHECKBYTE) {
|
|
parseshortcut(def[n].other, &(it->single.modifier), &(it->single.shortcut));
|
|
}
|
|
|
|
// parse static, radio, radioline, select
|
|
else if (it->type == mjITEM_STATIC ||
|
|
it->type == mjITEM_RADIO ||
|
|
it->type == mjITEM_RADIOLINE ||
|
|
it->type == mjITEM_SELECT) {
|
|
// init
|
|
it->multi.nelem = 0;
|
|
num = strlen(def[n].other);
|
|
i = 0;
|
|
|
|
// find names in '/n'-separated string
|
|
while (i < num) {
|
|
// check limit
|
|
if (it->multi.nelem >= mjMAXUIMULTI) {
|
|
mju_error("mjui_add: too many multi elements");
|
|
}
|
|
|
|
// find next '\n' or 0
|
|
start = i;
|
|
while (def[n].other[i] != '\n' && def[n].other[i] != 0) {
|
|
i++;
|
|
}
|
|
|
|
// check string
|
|
if (i == start || i-start >= mjMAXUINAME-1) {
|
|
mju_error("mjui_add: invalid multi element name");
|
|
}
|
|
|
|
// add
|
|
int ind = it->multi.nelem;
|
|
(it->multi.nelem)++;
|
|
strncpy(it->multi.name[ind], def[n].other+start, i-start);
|
|
it->multi.name[ind][i-start] = 0;
|
|
|
|
// advance
|
|
i++;
|
|
}
|
|
}
|
|
|
|
// parse slider
|
|
else if (it->type == mjITEM_SLIDERINT || it->type == mjITEM_SLIDERNUM) {
|
|
// read and check number
|
|
num = sscanf(def[n].other, "%lf %lf %lf", x, x+1, x+2);
|
|
if (num != 2 && num != 3) {
|
|
mju_error("mjui_add: slider expects 'min max [div]'");
|
|
}
|
|
|
|
// assign range
|
|
it->slider.range[0] = x[0];
|
|
it->slider.range[1] = x[1];
|
|
|
|
// assign divisions if provided, otherwise default
|
|
if (num == 3) {
|
|
it->slider.divisions = mjMAX(1, x[2]);
|
|
} else {
|
|
if (it->type == mjITEM_SLIDERINT) {
|
|
it->slider.divisions = mjMAX(1, x[1]-x[0]);
|
|
} else {
|
|
it->slider.divisions = 200;
|
|
}
|
|
}
|
|
}
|
|
|
|
// parse edit numeric
|
|
else if (it->type == mjITEM_EDITINT || it->type == mjITEM_EDITNUM || it->type == mjITEM_EDITFLOAT) {
|
|
// check mjMAXUIEDIT
|
|
if (mjMAXUIEDIT > 7) {
|
|
mju_error("internal error: mjMAXUIEDIT bigger than 7");
|
|
}
|
|
|
|
// parse nitem and up to 5 ranges
|
|
num = sscanf(def[n].other, "%lf %lf %lf %lf %lf %lf %lf %lf %lf %lf %lf %lf %lf %lf %lf",
|
|
x, x+1, x+2, x+3, x+4, x+5, x+6, x+7, x+8, x+9, x+10, x+11, x+12, x+13, x+14);
|
|
|
|
// check length, assign
|
|
if (num < 1) {
|
|
mju_error("mjui_add: edit length missing");
|
|
}
|
|
it->edit.nelem = mju_round((mjtNum)x[0]);
|
|
if (it->edit.nelem < 1 || it->edit.nelem > mjMAXUIEDIT) {
|
|
mju_error("mjui_add: invalid edit length");
|
|
}
|
|
if (it->edit.nelem*2 != (num-1) && num > 1) {
|
|
mju_error("mjui_add: incorrent number of edit ranges");
|
|
}
|
|
|
|
// copy ranges
|
|
if (num > 1) {
|
|
for (int i=0; i < it->edit.nelem; i++) {
|
|
it->edit.range[i][0] = x[1+2*i];
|
|
it->edit.range[i][1] = x[2+2*i];
|
|
}
|
|
}
|
|
}
|
|
|
|
// edit text: nothing to parse
|
|
else if (it->type == mjITEM_EDITTXT) {
|
|
// clear nelem, for style
|
|
it->edit.nelem = 0;
|
|
}
|
|
}
|
|
|
|
// invalid item type
|
|
else {
|
|
mju_error("mjui_add: invalid item type");
|
|
}
|
|
|
|
// advance to next definition
|
|
n++;
|
|
}
|
|
}
|
|
|
|
|
|
|
|
// Add definitions to UI section.
|
|
void mjui_addToSection(mjUI* ui, int sect, const mjuiDef* def) {
|
|
int nsect = ui->nsect;
|
|
ui->nsect = sect + 1;
|
|
mjui_add(ui, def);
|
|
ui->nsect = nsect;
|
|
}
|
|
|
|
|
|
|
|
// set item skip flags within section, but not in pass 0
|
|
static void setitemskip(mjuiSection* s, int pass) {
|
|
int skip = 0;
|
|
|
|
// process section items
|
|
for (int i = 0; i < s->nitem; ++i) {
|
|
mjuiItem* it = s->item + i;
|
|
|
|
// pass 0: nothing is skipped
|
|
if (pass == 0) {
|
|
it->skip = 0;
|
|
continue;
|
|
}
|
|
|
|
// item is a separator: update skip state for subsequent items
|
|
if (it->type == mjITEM_SEPARATOR) {
|
|
skip = (it->state == mjSEPCLOSED);
|
|
}
|
|
|
|
// item is not a separator: set skip state
|
|
else {
|
|
it->skip = skip;
|
|
}
|
|
}
|
|
}
|
|
|
|
|
|
|
|
// Compute UI sizes: internal fuction, may be called twice per resize
|
|
static void tryresize(mjUI* ui, const mjrContext* con) {
|
|
// scale theme sizes
|
|
int w_master = SCL(ui->spacing.total, con);
|
|
int w_scroll = SCL(ui->spacing.scroll, con);
|
|
int g_section = SCL(ui->spacing.section, con);
|
|
int g_itemside = SCL(ui->spacing.itemside, con);
|
|
int g_itemmid = SCL(ui->spacing.itemmid, con);
|
|
int g_itemver = SCL(ui->spacing.itemver, con);
|
|
int g_textver = SCL(ui->spacing.textver, con);
|
|
int g_label = SCL(ui->spacing.label, con);
|
|
|
|
// text element height, with gap above and below
|
|
int textheight = con->charHeight + 2*g_textver;
|
|
|
|
// column width
|
|
int colwidth = (w_master - w_scroll - 2*g_section - 2*g_itemside - g_itemmid)/2;
|
|
|
|
// pass 0 includes skipped items, pass 1 does not
|
|
int Height, MaxHeight;
|
|
for (int pass = 0; pass < 2; ++pass) {
|
|
// init UI heights
|
|
int height = 0;
|
|
int maxheight = 0;
|
|
|
|
// process sections
|
|
for (int n = 0; n < ui->nsect; n++) {
|
|
// vertical padding before section
|
|
height += g_section;
|
|
maxheight += g_section;
|
|
|
|
// get section pointer
|
|
mjuiSection* s = ui->sect + n;
|
|
|
|
// set item skip flags for section, depending on pass
|
|
setitemskip(s, pass);
|
|
|
|
// title rectangle
|
|
s->rtitle.left = g_section;
|
|
s->rtitle.width = w_master - w_scroll - 2 * g_section;
|
|
if (s->state == mjSECT_FIXED) { // fixed section: no title
|
|
s->rtitle.bottom = height;
|
|
s->rtitle.height = 0;
|
|
}
|
|
else { // regular section with title
|
|
s->rtitle.bottom = height + textheight;
|
|
s->rtitle.height = textheight;
|
|
}
|
|
|
|
// count title height
|
|
height += s->rtitle.height;
|
|
maxheight += s->rtitle.height;
|
|
|
|
// init content rectangle
|
|
s->rcontent.left = s->rtitle.left;
|
|
s->rcontent.width = s->rtitle.width;
|
|
s->rcontent.height = 0;
|
|
s->rcontent.bottom = 0;
|
|
|
|
// process items within section
|
|
for (int i = 0; i < s->nitem; i++) {
|
|
// get item pointer, clear rectangle
|
|
mjuiItem* it = s->item + i;
|
|
memset(&it->rect, 0, sizeof(mjrRect));
|
|
|
|
// item is skipped: nothing to do
|
|
if (it->skip) {
|
|
continue;
|
|
}
|
|
|
|
// vertical padding before item
|
|
s->rcontent.height += it->type == mjITEM_SEPARATOR ? g_section : g_itemver;
|
|
|
|
// packed pair of items
|
|
if (i < s->nitem - 1 && s->item[i + 1].type == it->type &&
|
|
(it->type == mjITEM_BUTTON ||
|
|
it->type == mjITEM_CHECKINT ||
|
|
it->type == mjITEM_CHECKBYTE)) {
|
|
// get next item pointer
|
|
mjuiItem* it1 = s->item + (i + 1);
|
|
|
|
// this item rectangle
|
|
it->rect.left = s->rcontent.left + g_itemside;
|
|
it->rect.width = colwidth;
|
|
it->rect.height = textheight;
|
|
|
|
// next item rectangle (set bottom here)
|
|
it1->rect.left = s->rcontent.left + g_itemside + colwidth + g_itemmid;
|
|
it1->rect.width = colwidth;
|
|
it1->rect.height = textheight;
|
|
it1->rect.bottom = height + s->rcontent.height + it->rect.height;
|
|
|
|
// advance
|
|
i++;
|
|
}
|
|
|
|
// single-line item
|
|
else {
|
|
// common left border (except for labeled controls at the end)
|
|
it->rect.left = s->rcontent.left + g_itemside;
|
|
|
|
// static
|
|
if (it->type == mjITEM_STATIC) {
|
|
it->rect.width = s->rcontent.width - 2 * g_itemside;
|
|
it->rect.height = (con->charHeight + g_textver) * it->multi.nelem;
|
|
}
|
|
|
|
// single column
|
|
else if (it->type == mjITEM_BUTTON ||
|
|
it->type == mjITEM_CHECKINT ||
|
|
it->type == mjITEM_CHECKBYTE) {
|
|
it->rect.width = colwidth;
|
|
it->rect.height = textheight;
|
|
}
|
|
|
|
// radio
|
|
else if (it->type == mjITEM_RADIO) {
|
|
int ncol = ui->radiocol ? ui->radiocol : 2;
|
|
int nrow = (it->multi.nelem - 1) / ncol + 1;
|
|
it->rect.width = s->rcontent.width - 2 * g_itemside;
|
|
it->rect.height = textheight * nrow;
|
|
}
|
|
|
|
// separator, select, slider, edit, radioline
|
|
else {
|
|
it->rect.width = s->rcontent.width - 2 * g_itemside;
|
|
it->rect.height = textheight;
|
|
}
|
|
|
|
// add room for label
|
|
if (it->name[0] &&
|
|
(it->type >= mjITEM_RADIO ||
|
|
it->type >= mjITEM_RADIOLINE ||
|
|
it->type == mjITEM_STATIC)) {
|
|
it->rect.left = s->rcontent.left + g_itemside + g_label;
|
|
it->rect.width = s->rcontent.width - (2 * g_itemside + g_label);
|
|
}
|
|
}
|
|
|
|
// set bottom, count height
|
|
it->rect.bottom = height + s->rcontent.height + it->rect.height;
|
|
s->rcontent.height += it->rect.height;
|
|
}
|
|
|
|
// vertical padding after last item, compute bottom
|
|
s->rcontent.height += g_itemver;
|
|
s->rcontent.bottom = height + s->rcontent.height;
|
|
|
|
// count content height
|
|
if (s->state != mjSECT_CLOSED) {
|
|
height += s->rcontent.height;
|
|
}
|
|
maxheight += s->rcontent.height;
|
|
}
|
|
|
|
// vertical padding after last section
|
|
height += g_section;
|
|
maxheight += g_section;
|
|
|
|
// save data: maxheight from pass 0, height from pass 1
|
|
if (pass == 0) {
|
|
MaxHeight = maxheight;
|
|
}
|
|
else {
|
|
Height = height;
|
|
}
|
|
}
|
|
|
|
// invert bottom for all sections and items
|
|
for (int n=0; n < ui->nsect; n++) {
|
|
// section
|
|
mjuiSection* s = ui->sect + n;
|
|
s->rtitle.bottom = Height - s->rtitle.bottom;
|
|
s->rcontent.bottom = Height - s->rcontent.bottom;
|
|
|
|
// items
|
|
for (int i=0; i < s->nitem; i++) {
|
|
s->item[i].rect.bottom = Height - s->item[i].rect.bottom;
|
|
}
|
|
}
|
|
|
|
// assign UI sizes
|
|
ui->width = w_master;
|
|
ui->height = Height;
|
|
ui->maxheight = MaxHeight;
|
|
}
|
|
|
|
|
|
|
|
// insertion sort of groups of ints: increasing order of leading int
|
|
static void insertionsortgroup(int* list, int num, int stride) {
|
|
// allocate buffer of 10 ints, cannot handle more
|
|
if (stride > 10) {
|
|
mju_error("insertionsortgroup cannot handle stride greater than 10");
|
|
}
|
|
int x[10];
|
|
|
|
for (int i = 1; i < num; i++) {
|
|
memcpy(x, list + i * stride, sizeof(int) * stride);
|
|
|
|
int j = i - 1;
|
|
while (j >= 0 && list[j * stride] > x[0]) {
|
|
memcpy(list + (j + 1) * stride, list + j * stride, sizeof(int) * stride);
|
|
j--;
|
|
}
|
|
|
|
memcpy(list + (j + 1) * stride, x, sizeof(int) * stride);
|
|
}
|
|
}
|
|
|
|
|
|
|
|
// Compute UI sizes.
|
|
void mjui_resize(mjUI* ui, const mjrContext* con) {
|
|
// get maximum buffer size allowed by OpenGL driver
|
|
int maxBufferSize = 0;
|
|
glGetIntegerv(GL_MAX_RENDERBUFFER_SIZE, &maxBufferSize);
|
|
|
|
// USED FOR TESTING OF SMALL BUFFER SIZES
|
|
// maxBufferSize = 3000;
|
|
|
|
// resize with current section states, clamp maxheight
|
|
tryresize(ui, con);
|
|
ui->maxheight = mjMIN(ui->maxheight, maxBufferSize);
|
|
|
|
// if height is too large, close some sections
|
|
if (ui->height > ui->maxheight) {
|
|
// init new height with section gaps
|
|
int hnew = (ui->nsect + 1) * SCL(ui->spacing.section, con);
|
|
|
|
// add titles of regular sections and contents of fixed sections
|
|
for (int n = 0; n < ui->nsect; ++n) {
|
|
if (ui->sect[n].state == mjSECT_FIXED) {
|
|
hnew += ui->sect[n].rcontent.height;
|
|
}
|
|
else {
|
|
hnew += ui->sect[n].rtitle.height;
|
|
}
|
|
}
|
|
|
|
// if fixed height is too big, nothing we can do
|
|
if (hnew > ui->maxheight) {
|
|
mju_error("fixed section height already too big, closing sections cannot help");
|
|
}
|
|
|
|
// sort open sections by lastclick
|
|
int nopen = 0;
|
|
int sortbuf[2 * mjMAXUISECT] = { 0 };
|
|
for (int n = 0; n < ui->nsect; ++n) {
|
|
if (ui->sect[n].state == mjSECT_OPEN) {
|
|
sortbuf[2 * nopen] = ui->sect[n].lastclick;
|
|
sortbuf[2 * nopen + 1] = n;
|
|
++nopen;
|
|
}
|
|
}
|
|
insertionsortgroup(sortbuf, nopen, 2);
|
|
|
|
// nothing is open; SHOULD NOT OCCUR
|
|
if (nopen == 0) {
|
|
mju_error("internal error: expected some sections to be open");
|
|
}
|
|
|
|
// keep most recent sections: as many as can fit in maxheight
|
|
for (int i = nopen - 1; i >= 0; --i) {
|
|
// section fits: add height
|
|
if (hnew + ui->sect[sortbuf[2 * i + 1]].rcontent.height <= ui->maxheight) {
|
|
hnew += ui->sect[sortbuf[2 * i + 1]].rcontent.height;
|
|
}
|
|
|
|
// section does not fit: mark for closing
|
|
else {
|
|
sortbuf[2 * i] = -1;
|
|
}
|
|
}
|
|
|
|
// close sections that were marked
|
|
for (int i = 0; i < nopen; ++i) {
|
|
if (sortbuf[2 * i] == -1) {
|
|
ui->sect[sortbuf[2 * i + 1]].state = mjSECT_CLOSED;
|
|
}
|
|
}
|
|
|
|
// resize with new section states, clamp maxheight again
|
|
tryresize(ui, con);
|
|
ui->maxheight = mjMIN(ui->maxheight, maxBufferSize);
|
|
|
|
// make sure tryresize did what we expected; SHOULD NOT OCCUR
|
|
if (ui->height != hnew) {
|
|
mju_error("internal error: tryresize produced unexpeced ui height");
|
|
}
|
|
}
|
|
}
|
|
|
|
|
|
|
|
// predicate handler
|
|
static int evalpredicate(int state, mjfItemEnable predicate, void* userdata) {
|
|
if (state <= 0) {
|
|
return 0;
|
|
} else if (state == 1 || predicate == NULL) {
|
|
return 1;
|
|
} else {
|
|
return predicate(state, userdata);
|
|
}
|
|
}
|
|
|
|
|
|
|
|
// draw shortcut text
|
|
static void shortcuthelp(mjrRect r, int modifier, int shortcut,
|
|
const mjUI* ui, const mjrContext* con) {
|
|
// map of key codes and corresponding names
|
|
#define NMAP 27
|
|
const struct {
|
|
int key;
|
|
const char* value;
|
|
} keymap[NMAP] = {
|
|
{32 , "Space"},
|
|
{mjKEY_ESCAPE , "Esc"},
|
|
{mjKEY_ENTER , "Enter"},
|
|
{mjKEY_TAB , "Tab"},
|
|
{mjKEY_BACKSPACE , "BackSpace"},
|
|
{mjKEY_INSERT , "Ins"},
|
|
{mjKEY_DELETE , "Del"},
|
|
{mjKEY_RIGHT , "Right"},
|
|
{mjKEY_LEFT , "Left"},
|
|
{mjKEY_DOWN , "Down"},
|
|
{mjKEY_UP , "Up"},
|
|
{mjKEY_PAGE_UP , "PgUp"},
|
|
{mjKEY_PAGE_DOWN , "PgDn"},
|
|
{mjKEY_HOME , "Home"},
|
|
{mjKEY_END , "End"},
|
|
{mjKEY_F1 , "F1"},
|
|
{mjKEY_F2 , "F2"},
|
|
{mjKEY_F3 , "F3"},
|
|
{mjKEY_F4 , "F4"},
|
|
{mjKEY_F5 , "F5"},
|
|
{mjKEY_F6 , "F6"},
|
|
{mjKEY_F7 , "F7"},
|
|
{mjKEY_F8 , "F8"},
|
|
{mjKEY_F9 , "F9"},
|
|
{mjKEY_F10 , "F10"},
|
|
{mjKEY_F11 , "F11"},
|
|
{mjKEY_F12 , "F12"}
|
|
};
|
|
|
|
// key: ascii or decode map
|
|
char key[10] = "";
|
|
if (shortcut > 32 && shortcut <= 126) {
|
|
key[0] = (char)shortcut;
|
|
key[1] = 0;
|
|
} else {
|
|
for (int i=0; i < NMAP; i++) {
|
|
if (keymap[i].key == shortcut) {
|
|
mjSTRNCPY(key, keymap[i].value);
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
|
|
// modifier
|
|
char text[50] = "";
|
|
if (modifier == 1) {
|
|
mjSTRNCPY(text, "Ctrl ");
|
|
} else if (modifier == 2) {
|
|
mjSTRNCPY(text, "Shift ");
|
|
} else if (modifier == 4) {
|
|
mjSTRNCPY(text, "Alt ");
|
|
}
|
|
|
|
// combine
|
|
strcat(text, key);
|
|
|
|
// make rectangle for shortcut
|
|
int g_textver = SCL(ui->spacing.textver, con);
|
|
int width = textwidth(text, con, -1) + 2*g_textver;
|
|
r.left += (r.width-width);
|
|
r.width = width;
|
|
r.bottom += g_textver;
|
|
r.height -= 2*g_textver;
|
|
|
|
// render
|
|
drawrectangle(r, ui->color.shortcut, NULL, con);
|
|
drawtext(text, r.left+g_textver, r.bottom,
|
|
r.width, ui->color.fontactive, con);
|
|
}
|
|
|
|
|
|
|
|
// Update specific section/item; -1: update all.
|
|
void mjui_update(int section, int item, const mjUI* ui,
|
|
const mjuiState* state, const mjrContext* con) {
|
|
int g_texthor = SCL(ui->spacing.texthor, con);
|
|
int g_textver = SCL(ui->spacing.textver, con);
|
|
int g_label = SCL(ui->spacing.label, con);
|
|
int g_itemside = SCL(ui->spacing.itemside, con);
|
|
int g_itemmid = SCL(ui->spacing.itemmid, con);
|
|
int cellheight = con->charHeight + 2*g_textver;
|
|
const float* rgbpane = ui->color.sectpane;
|
|
|
|
// locate mouse in ui
|
|
int msect, mitem;
|
|
findmouse(ui, state, con, &msect, &mitem);
|
|
|
|
// start rendering
|
|
mjr_setAux(ui->auxid, con);
|
|
mjrRect rgl = { 0, 0, ui->width, ui->height };
|
|
initOpenGL(&rgl, con);
|
|
|
|
// all sections: clear background
|
|
if (section < 0) {
|
|
glClearColor(ui->color.master[0], ui->color.master[1],
|
|
ui->color.master[2], 1);
|
|
glClear(GL_COLOR_BUFFER_BIT);
|
|
}
|
|
|
|
// select sections
|
|
int start_section = 0;
|
|
int num_section = ui->nsect;
|
|
if (section >= 0) {
|
|
start_section = section;
|
|
num_section = 1;
|
|
}
|
|
|
|
// draw section(s)
|
|
mjtNum sel;
|
|
mjrRect r;
|
|
char text[mjMAXUITEXT];
|
|
int sep[mjMAXUIMULTI+1];
|
|
for (int n=start_section; n < start_section+num_section; n++) {
|
|
// get section pointer
|
|
const mjuiSection* s = ui->sect + n;
|
|
|
|
// standard maxwidth for text
|
|
int maxwidth = (s->rtitle.width - 2*g_itemside - g_itemmid)/2 - 2*g_texthor;
|
|
|
|
// redraw section title and pane
|
|
if (section < 0 || item < 0) {
|
|
r = s->rtitle;
|
|
|
|
// title shown
|
|
if (s->state != mjSECT_FIXED) {
|
|
// section without checkbox
|
|
if (s->checkbox == 0) {
|
|
// interpolated rectangle
|
|
glBegin(GL_QUADS);
|
|
glColor3fv(ui->color.secttitle2);
|
|
glVertex2i(r.left, r.bottom);
|
|
glVertex2i(r.left + r.width, r.bottom);
|
|
glColor3fv(ui->color.secttitle);
|
|
glVertex2i(r.left + r.width, r.bottom + r.height);
|
|
glVertex2i(r.left, r.bottom + r.height);
|
|
glEnd();
|
|
|
|
// symbol and text
|
|
drawsymbol(r, s->state, 0, ui, con);
|
|
drawtext(s->name, r.left + g_texthor,
|
|
r.bottom + g_textver, 2 * maxwidth,
|
|
ui->color.sectfont, con);
|
|
}
|
|
|
|
// section with checkbox
|
|
else {
|
|
glBegin(GL_QUADS);
|
|
glColor3fv(ui->color.secttitlecheck2);
|
|
glVertex2i(r.left, r.bottom);
|
|
glVertex2i(r.left + r.width, r.bottom);
|
|
glColor3fv(ui->color.secttitlecheck);
|
|
glVertex2i(r.left + r.width, r.bottom + r.height);
|
|
glVertex2i(r.left, r.bottom + r.height);
|
|
glEnd();
|
|
|
|
// symbol and text with offset
|
|
drawsymbol(r, s->state, 1, ui, con);
|
|
drawtext(s->name, r.left + r.height,
|
|
r.bottom + g_textver, 2 * maxwidth - r.height,
|
|
ui->color.sectfont, con);
|
|
|
|
// draw checkmark as specified
|
|
if (s->checkbox > 1) {
|
|
int cgap = r.height / 4;
|
|
mjrRect cr = { r.left + cgap, r.bottom + cgap, r.height - 2 * cgap, r.height - 2 * cgap };
|
|
float rgb[3] = {
|
|
0.5f * (ui->color.secttitlecheck[0] + ui->color.secttitlecheck2[0]),
|
|
0.5f * (ui->color.secttitlecheck[1] + ui->color.secttitlecheck2[1]),
|
|
0.5f * (ui->color.secttitlecheck[2] + ui->color.secttitlecheck2[2])
|
|
};
|
|
drawrectangle(cr, ui->color.sectsymbol,
|
|
s->checkbox == 2 ? rgb : NULL, con);
|
|
}
|
|
}
|
|
|
|
// shortcut
|
|
if (ui->mousehelp && s->shortcut) {
|
|
shortcuthelp(r, s->modifier, s->shortcut, ui, con);
|
|
}
|
|
}
|
|
|
|
// content pane, active only
|
|
if (s->state != mjSECT_CLOSED) {
|
|
drawrectangle(s->rcontent, ui->color.sectpane, NULL, con);
|
|
}
|
|
|
|
// round corners
|
|
mjrRect rround = s->rtitle;
|
|
if (s->state == mjSECT_FIXED) {
|
|
rround = s->rcontent;
|
|
}
|
|
else if (s->state == mjSECT_OPEN) {
|
|
rround.bottom = s->rcontent.bottom;
|
|
rround.height = s->rtitle.height + s->rcontent.height;
|
|
}
|
|
roundcorner(rround, 0, 0, ui, con);
|
|
}
|
|
|
|
// closed: skip items
|
|
if (!s->state) {
|
|
continue;
|
|
}
|
|
|
|
// select items
|
|
int start_item = 0;
|
|
int num_item = s->nitem;
|
|
if (section >= 0 && item >= 0) {
|
|
start_item = item;
|
|
num_item = 1;
|
|
}
|
|
|
|
// draw item(s)
|
|
for (int i=start_item; i < start_item+num_item; i++) {
|
|
// get item pointer
|
|
const mjuiItem* it = s->item + i;
|
|
|
|
// zero size: skip
|
|
if (it->rect.height == 0) {
|
|
continue;
|
|
}
|
|
|
|
// set rgb pointers to inactive state
|
|
const float* rgbfont = ui->color.fontinactive;
|
|
const float* rgbdecor = ui->color.decorinactive;
|
|
const float* rgbdecor2 = ui->color.decorinactive2;
|
|
|
|
// get actual state, adjust font rgb
|
|
int state = evalpredicate(it->state, ui->predicate, ui->userdata);
|
|
if (state) {
|
|
rgbfont = ui->color.fontactive;
|
|
}
|
|
|
|
// type-specific draw
|
|
switch (it->type) {
|
|
case mjITEM_SEPARATOR:
|
|
// background
|
|
r = it->rect;
|
|
glBegin(GL_QUADS);
|
|
glColor3fv(ui->color.separator2);
|
|
glVertex2i(r.left, r.bottom);
|
|
glVertex2i(r.left+r.width, r.bottom);
|
|
glColor3fv(ui->color.separator);
|
|
glVertex2i(r.left+r.width, r.bottom+r.height);
|
|
glVertex2i(r.left, r.bottom+r.height);
|
|
glEnd();
|
|
|
|
// name
|
|
drawtext(it->name,
|
|
it->rect.left+g_texthor,
|
|
it->rect.bottom+g_textver,
|
|
it->rect.width-2*g_texthor, ui->color.sectfont, con);
|
|
|
|
// symbol and round corners for collapsible
|
|
if (it->state >= mjSEPCLOSED) {
|
|
int flg_open = (it->state == mjSEPCLOSED + 1);
|
|
drawsymbol(it->rect, flg_open, 2, ui, con);
|
|
roundcorner(it->rect, flg_open, 1, ui, con);
|
|
}
|
|
break;
|
|
|
|
case mjITEM_STATIC:
|
|
r = it->rect;
|
|
|
|
// name: at top
|
|
drawtext(it->name,
|
|
s->rcontent.left+g_itemside+g_texthor,
|
|
r.bottom+g_textver+(it->multi.nelem-1)*(con->charHeight+g_textver),
|
|
g_label-2*g_texthor, rgbfont, con);
|
|
|
|
// background
|
|
drawrectangle(r, rgbpane, rgbpane, con);
|
|
|
|
// text lines
|
|
for (int k=0; k < it->multi.nelem; k++) {
|
|
drawtext(it->multi.name[k],
|
|
r.left+g_texthor,
|
|
r.bottom+g_textver+(it->multi.nelem-k-1)*(con->charHeight+g_textver),
|
|
r.width-2*g_texthor, rgbfont, con);
|
|
}
|
|
break;
|
|
|
|
case mjITEM_BUTTON:
|
|
if (state) {
|
|
rgbdecor = ui->color.button;
|
|
}
|
|
|
|
// outline or filled, depending on mouse
|
|
if (ui->mousesect == n+1 && ui->mouseitem == i && msect == n+1 && mitem == i) {
|
|
drawoval(it->rect, rgbdecor, NULL, con);
|
|
} else {
|
|
drawoval(it->rect, rgbdecor, rgbpane, con);
|
|
}
|
|
|
|
// name
|
|
drawtext(it->name,
|
|
it->rect.left+g_texthor,
|
|
it->rect.bottom+g_textver,
|
|
maxwidth, rgbfont, con);
|
|
|
|
// shortcut
|
|
if (ui->mousehelp && it->single.shortcut) {
|
|
shortcuthelp(it->rect, it->single.modifier, it->single.shortcut, ui, con);
|
|
}
|
|
break;
|
|
|
|
case mjITEM_CHECKINT:
|
|
case mjITEM_CHECKBYTE:
|
|
{
|
|
if (state) {
|
|
rgbdecor = ui->color.check;
|
|
}
|
|
|
|
// get value according to type
|
|
int k;
|
|
if (it->type == mjITEM_CHECKINT) {
|
|
k = *(int*)it->pdata;
|
|
} else {
|
|
k = *(mjtByte*)it->pdata;
|
|
}
|
|
|
|
// filled or outline
|
|
if (k) {
|
|
drawrectangle(it->rect, rgbdecor, NULL, con);
|
|
} else {
|
|
drawrectangle(it->rect, rgbdecor, rgbpane, con);
|
|
}
|
|
|
|
// name
|
|
drawtext(it->name,
|
|
it->rect.left+g_texthor,
|
|
it->rect.bottom+g_textver,
|
|
maxwidth, rgbfont, con);
|
|
|
|
// shortcut
|
|
if (ui->mousehelp && it->single.shortcut)
|
|
shortcuthelp(it->rect, it->single.modifier, it->single.shortcut,
|
|
ui, con);
|
|
}
|
|
break;
|
|
|
|
case mjITEM_RADIO:
|
|
if (state) {
|
|
rgbdecor = ui->color.radio;
|
|
}
|
|
|
|
// name: at top
|
|
drawtext(it->name,
|
|
s->rcontent.left+g_itemside+g_texthor,
|
|
it->rect.bottom+it->rect.height-cellheight+g_textver,
|
|
g_label-2*g_texthor, rgbfont, con);
|
|
|
|
// outline
|
|
drawrectangle(it->rect, rgbdecor, rgbpane, con);
|
|
|
|
// fill selection
|
|
r = radioelement(it, *(int*)it->pdata, ui, con);
|
|
drawrectangle(r, rgbdecor, NULL, con);
|
|
|
|
// element names
|
|
for (int k=0; k < it->multi.nelem; k++) {
|
|
r = radioelement(it, k, ui, con);
|
|
drawtext(it->multi.name[k],
|
|
r.left+g_texthor,
|
|
r.bottom+g_textver,
|
|
r.width-2*g_texthor, rgbfont, con);
|
|
}
|
|
break;
|
|
|
|
case mjITEM_RADIOLINE:
|
|
{
|
|
if (state) {
|
|
rgbdecor = ui->color.radio;
|
|
}
|
|
|
|
// name
|
|
drawtext(it->name,
|
|
s->rcontent.left+g_itemside+g_texthor,
|
|
it->rect.bottom+g_textver,
|
|
g_label-2*g_texthor, rgbfont, con);
|
|
|
|
// outline
|
|
drawrectangle(it->rect, rgbdecor, rgbpane, con);
|
|
|
|
// make separators
|
|
makeradioline(it, con, sep);
|
|
|
|
// fill selected
|
|
int k = *(int*)it->pdata;
|
|
r = it->rect;
|
|
r.left += sep[k];
|
|
r.width = sep[k+1] - sep[k];
|
|
drawrectangle(r, rgbdecor, NULL, con);
|
|
|
|
// element names
|
|
for (int k=0; k < it->multi.nelem; k++) {
|
|
// compute rectangle for element
|
|
r = it->rect;
|
|
r.left += sep[k];
|
|
r.width = sep[k+1] - sep[k];
|
|
|
|
// draw centered
|
|
drawtextrect(r, it->multi.name[k], rgbfont, con);
|
|
}
|
|
}
|
|
break;
|
|
|
|
case mjITEM_SELECT:
|
|
if (state) {
|
|
rgbdecor = ui->color.select;
|
|
}
|
|
|
|
// name
|
|
drawtext(it->name,
|
|
s->rcontent.left+g_itemside+g_texthor,
|
|
it->rect.bottom+g_textver,
|
|
g_label-2*g_texthor, rgbfont, con);
|
|
|
|
// box
|
|
drawrectangle(it->rect, rgbdecor, NULL, con);
|
|
|
|
// value: only if non-empty
|
|
if (it->multi.nelem > 0) {
|
|
drawtext(it->multi.name[*(int*)it->pdata],
|
|
it->rect.left+g_texthor,
|
|
it->rect.bottom+g_textver,
|
|
it->rect.width-2*g_texthor, rgbfont, con);
|
|
}
|
|
|
|
// draw tracking in mjui_render()
|
|
break;
|
|
|
|
case mjITEM_SLIDERINT:
|
|
case mjITEM_SLIDERNUM:
|
|
if (state) {
|
|
rgbdecor = ui->color.slider;
|
|
rgbdecor2 = ui->color.slider2;
|
|
}
|
|
|
|
// compute relative slider position
|
|
if (it->type == mjITEM_SLIDERINT) {
|
|
sel = (mjtNum)(*(int*)it->pdata);
|
|
} else {
|
|
sel = *(mjtNum*)it->pdata;
|
|
}
|
|
sel = (sel - it->slider.range[0]) /
|
|
mjMAX(1e-10, (it->slider.range[1] - it->slider.range[0]));
|
|
sel = mjMIN(1, mjMAX(0, sel));
|
|
|
|
// fill slider with two colors
|
|
r = it->rect;
|
|
drawrectangle(r, rgbdecor, NULL, con);
|
|
r.width = mju_round(r.width*sel);
|
|
drawrectangle(r, rgbdecor2, NULL, con);
|
|
|
|
// show divisions if not too many
|
|
if (it->slider.divisions <= 20) {
|
|
// repare rectangle
|
|
r.width = SCL(2, con);
|
|
r.height = g_textver;
|
|
|
|
// draw ticks
|
|
for (int k=1; k < (int)it->slider.divisions; k++) {
|
|
r.left = it->rect.left - r.width/2 +
|
|
it->rect.width*k/it->slider.divisions;
|
|
drawrectangle(r, rgbpane, NULL, con);
|
|
}
|
|
}
|
|
|
|
// name
|
|
drawtext(it->name,
|
|
s->rcontent.left+g_itemside+g_texthor,
|
|
it->rect.bottom+g_textver,
|
|
g_label-2*g_texthor, rgbfont, con);
|
|
|
|
// value
|
|
if (it->type == mjITEM_SLIDERINT) {
|
|
mjSNPRINTF(text, "%d", *(int*)it->pdata);
|
|
} else {
|
|
mjSNPRINTF(text, "%.3g", *(mjtNum*)it->pdata);
|
|
}
|
|
drawtext(text,
|
|
it->rect.left+g_texthor,
|
|
it->rect.bottom+g_textver,
|
|
it->rect.width-2*g_texthor, rgbfont, con);
|
|
break;
|
|
|
|
case mjITEM_EDITINT:
|
|
case mjITEM_EDITNUM:
|
|
case mjITEM_EDITFLOAT:
|
|
case mjITEM_EDITTXT:
|
|
if (state) {
|
|
rgbdecor = ui->color.edit;
|
|
}
|
|
|
|
// name
|
|
drawtext(it->name,
|
|
s->rcontent.left+g_itemside+g_texthor,
|
|
it->rect.bottom+g_textver,
|
|
g_label-2*g_texthor, rgbfont, con);
|
|
|
|
// activated
|
|
if (ui->editsect > 0 && ui->editsect == n+1 && ui->edititem == i) {
|
|
// fill box, indicate valid state with color
|
|
if (checkedit(ui->edittext, it) == 0) {
|
|
drawrectangle(it->rect, rgbdecor, NULL, con);
|
|
} else {
|
|
drawrectangle(it->rect, ui->color.edit2, NULL, con);
|
|
}
|
|
|
|
// show cursor
|
|
int k = textwidth(ui->edittext + ui->editscroll, con,
|
|
ui->editcursor - ui->editscroll);
|
|
r.left = it->rect.left + g_texthor + k - SCL(1, con);
|
|
r.width = 2*SCL(1, con);
|
|
r.bottom = it->rect.bottom + g_textver/2;
|
|
r.height = it->rect.height - 2*(g_textver/2);
|
|
drawrectangle(r, ui->color.cursor, NULL, con);
|
|
|
|
// value from edittext, with scroll
|
|
drawtext(ui->edittext + ui->editscroll,
|
|
it->rect.left+g_texthor,
|
|
it->rect.bottom+g_textver,
|
|
it->rect.width-2*g_texthor, rgbfont, con);
|
|
}
|
|
|
|
// not activated
|
|
else {
|
|
// outline box
|
|
drawrectangle(it->rect, rgbdecor, rgbpane, con);
|
|
|
|
// value from pdata, no scroll
|
|
array2text(text, it);
|
|
drawtext(text,
|
|
it->rect.left+g_texthor,
|
|
it->rect.bottom+g_textver,
|
|
it->rect.width-2*g_texthor, rgbfont, con);
|
|
}
|
|
break;
|
|
|
|
default:
|
|
mju_error("mjui_update: internal error: unexpected item type");
|
|
}
|
|
}
|
|
}
|
|
|
|
// stop rendering
|
|
mjr_restoreBuffer(con);
|
|
}
|
|
|
|
|
|
|
|
// Handle UI event, return pointer to changed item, NULL if no change.
|
|
mjuiItem* mjui_event(mjUI* ui, mjuiState* state, const mjrContext* con) {
|
|
int i, key, change = 0;
|
|
mjuiItem* it;
|
|
ui->editchanged = NULL;
|
|
|
|
// count mouse clicks over UI
|
|
if (state->type == mjEVENT_PRESS) {
|
|
++ui->mouseclicks;
|
|
}
|
|
|
|
// non-left mouse events: handle shortcut help
|
|
if ((state->type == mjEVENT_PRESS || state->type == mjEVENT_MOVE ||
|
|
state->type == mjEVENT_RELEASE) && state->button != mjBUTTON_LEFT) {
|
|
if (state->button == mjBUTTON_RIGHT) {
|
|
if (state->type == mjEVENT_PRESS) {
|
|
ui->mousehelp = 1;
|
|
mjui_update(-1, -1, ui, state, con);
|
|
} else if (state->type == mjEVENT_RELEASE) {
|
|
ui->mousehelp = 0;
|
|
mjui_update(-1, -1, ui, state, con);
|
|
}
|
|
}
|
|
|
|
return NULL;
|
|
}
|
|
|
|
// get current mouse section and item
|
|
int sect_cur = -1;
|
|
int item_cur = -1;
|
|
mjuiItem* it_cur = NULL;
|
|
findmouse(ui, state, con, §_cur, &item_cur);
|
|
if (sect_cur > 0 && item_cur >= 0) {
|
|
it_cur = ui->sect[sect_cur-1].item + item_cur;
|
|
}
|
|
|
|
// update section lastclick
|
|
if (sect_cur > 0 && state->type == mjEVENT_PRESS) {
|
|
ui->sect[sect_cur - 1].lastclick = ui->mouseclicks;
|
|
}
|
|
|
|
// get recorded mouse section and item
|
|
int sect_rec = ui->mousesect;
|
|
int item_rec = -1;
|
|
mjuiItem* it_rec = NULL;
|
|
if (sect_rec > 0) {
|
|
item_rec = ui->mouseitem;
|
|
it_rec = ui->sect[sect_rec-1].item + item_rec;
|
|
}
|
|
|
|
// get edit section and item
|
|
int sect_edit = ui->editsect;
|
|
int item_edit = -1;
|
|
mjuiItem* it_edit = NULL;
|
|
if (sect_edit > 0) {
|
|
item_edit = ui->edititem;
|
|
it_edit = ui->sect[sect_edit-1].item + item_edit;
|
|
}
|
|
|
|
// process according to type
|
|
switch (state->type) {
|
|
case mjEVENT_MOVE:
|
|
// move scrollbar
|
|
if (sect_rec == -1) {
|
|
ui->scroll -= mju_round((state->dy * ui->height) /
|
|
state->rect[ui->rectid].height);
|
|
ui->scroll = mjMAX(0, mjMIN(ui->scroll,
|
|
ui->height-state->rect[ui->rectid].height));
|
|
}
|
|
|
|
// item
|
|
else if (it_rec) {
|
|
// move slider
|
|
if (it_rec->type == mjITEM_SLIDERINT || it_rec->type == mjITEM_SLIDERNUM) {
|
|
setslider(it_rec, ui, state, con);
|
|
|
|
// redraw, return change
|
|
mjui_update(sect_rec-1, item_rec, ui, state, con);
|
|
return it_rec;
|
|
}
|
|
|
|
// move edit
|
|
else if (it_rec->type == mjITEM_EDITINT ||
|
|
it_rec->type == mjITEM_EDITNUM ||
|
|
it_rec->type == mjITEM_EDITFLOAT ||
|
|
it_rec->type == mjITEM_EDITTXT) {
|
|
setcursor(it_rec->rect, ui, state, con);
|
|
}
|
|
|
|
// redraw (any item type)
|
|
mjui_update(sect_rec-1, item_rec, ui, state, con);
|
|
}
|
|
return NULL;
|
|
|
|
case mjEVENT_PRESS:
|
|
// edit in progress
|
|
if (sect_edit > 0) {
|
|
// same: adjust cursor position
|
|
if (sect_edit == sect_cur && item_edit == item_cur) {
|
|
// start mouse tracking
|
|
ui->mousesect = sect_cur;
|
|
ui->mouseitem = item_cur;
|
|
|
|
// adjust cursor and scroll
|
|
setcursor(it_edit->rect, ui, state, con);
|
|
|
|
// redraw, return no change
|
|
mjui_update(sect_edit-1, item_edit, ui, state, con);
|
|
return NULL;
|
|
}
|
|
|
|
// different: instantiate, record editchange
|
|
if (text2array(ui->edittext, it_edit) == 0) {
|
|
ui->editchanged = it_edit;
|
|
}
|
|
|
|
// stop editing, even if invalid
|
|
ui->editsect = 0;
|
|
|
|
// redraw
|
|
mjui_update(sect_edit-1, item_edit, ui, state, con);
|
|
}
|
|
|
|
// free mouse focus, just in case
|
|
ui->mousesect = 0;
|
|
|
|
// start scrollbar drag
|
|
if (sect_cur == -1) {
|
|
ui->mousesect = -1;
|
|
ui->mouseitem = 0;
|
|
}
|
|
|
|
// scroll down
|
|
else if (sect_cur == -2) {
|
|
ui->scroll = ui->height - state->rect[ui->rectid].height;
|
|
}
|
|
|
|
// scroll up
|
|
else if (sect_cur == -3) {
|
|
ui->scroll = 0;
|
|
}
|
|
|
|
// section title
|
|
else if (sect_cur > 0 && item_cur < 0) {
|
|
mjuiSection* se = ui->sect + sect_cur - 1;
|
|
|
|
// handle section checkbox
|
|
if (item_cur == -2 && se->checkbox > 0) {
|
|
ui->mousesectcheck = sect_cur;
|
|
return NULL; // leave it to user, because sections may interact
|
|
}
|
|
else {
|
|
ui->mousesectcheck = 0;
|
|
}
|
|
|
|
// double-click: make all sections like this (exclude fixed)
|
|
if (state->doubleclick) {
|
|
for (int i=0; i < ui->nsect; i++) {
|
|
if (ui->sect[i].state != mjSECT_FIXED && se->state != mjSECT_FIXED) {
|
|
ui->sect[i].state = se->state;
|
|
}
|
|
}
|
|
}
|
|
|
|
// single click: toggle section state (exclude fixed)
|
|
else {
|
|
if (se->state == mjSECT_OPEN) {
|
|
se->state = mjSECT_CLOSED;
|
|
}
|
|
else if (se->state == mjSECT_CLOSED) {
|
|
se->state = mjSECT_OPEN;
|
|
}
|
|
}
|
|
|
|
// resize and redraw all
|
|
mjui_resize(ui, con);
|
|
mjui_update(-1, -1, ui, state, con);
|
|
}
|
|
|
|
// item
|
|
else if (sect_cur > 0 && item_cur >= 0) {
|
|
// nothing to do for inactive item
|
|
if (!evalpredicate(it_cur->state, ui->predicate, ui->userdata)) {
|
|
return ui->editchanged;
|
|
}
|
|
|
|
// process according to type, only if active
|
|
switch (it_cur->type) {
|
|
case mjITEM_SEPARATOR:
|
|
// expanded: collapse
|
|
if (it_cur->state == mjSEPCLOSED+1) {
|
|
it_cur->state = mjSEPCLOSED;
|
|
mjui_resize(ui, con);
|
|
mjui_update(-1, -1, ui, state, con);
|
|
}
|
|
|
|
// collapsed: expand
|
|
else if (it_cur->state == mjSEPCLOSED) {
|
|
it_cur->state = mjSEPCLOSED+1;
|
|
mjui_resize(ui, con);
|
|
mjui_update(-1, -1, ui, state, con);
|
|
}
|
|
|
|
// nothing else to do for separator
|
|
return ui->editchanged;
|
|
|
|
case mjITEM_BUTTON:
|
|
// start tracking
|
|
ui->mousesect = sect_cur;
|
|
ui->mouseitem = item_cur;
|
|
change = 1;
|
|
break;
|
|
|
|
case mjITEM_CHECKINT:
|
|
// toggle check
|
|
*(int*)it_cur->pdata = 1 - *(int*)it_cur->pdata;
|
|
change = 1;
|
|
break;
|
|
|
|
case mjITEM_CHECKBYTE:
|
|
// toggle check
|
|
*(mjtByte*)it_cur->pdata = 1 - *(mjtByte*)it_cur->pdata;
|
|
change = 1;
|
|
break;
|
|
|
|
case mjITEM_RADIO:
|
|
// set selected element
|
|
i = findradio(it_cur, ui, state, con);
|
|
if (i >= 0) {
|
|
*(int*)it_cur->pdata = i;
|
|
change = 1;
|
|
}
|
|
break;
|
|
|
|
case mjITEM_RADIOLINE:
|
|
// set selected element
|
|
i = findradioline(it_cur, ui, state, con);
|
|
if (i >= 0) {
|
|
*(int*)it_cur->pdata = i;
|
|
change = 1;
|
|
}
|
|
break;
|
|
|
|
case mjITEM_SELECT:
|
|
// start tracking
|
|
ui->mousesect = sect_cur;
|
|
ui->mouseitem = item_cur;
|
|
break;
|
|
|
|
case mjITEM_SLIDERINT:
|
|
case mjITEM_SLIDERNUM:
|
|
// set value, start tracking
|
|
setslider(it_cur, ui, state, con);
|
|
ui->mousesect = sect_cur;
|
|
ui->mouseitem = item_cur;
|
|
change = 1;
|
|
break;
|
|
|
|
case mjITEM_EDITINT:
|
|
case mjITEM_EDITNUM:
|
|
case mjITEM_EDITFLOAT:
|
|
case mjITEM_EDITTXT:
|
|
// set edit text, clear scroll
|
|
array2text(ui->edittext, it_cur);
|
|
ui->editscroll = 0;
|
|
|
|
// set editcursor, adjust editscroll
|
|
setcursor(it_cur->rect, ui, state, con);
|
|
|
|
// start tracking: both edit and mouse
|
|
ui->editsect = sect_cur;
|
|
ui->edititem = item_cur;
|
|
ui->mousesect = sect_cur;
|
|
ui->mouseitem = item_cur;
|
|
break;
|
|
}
|
|
|
|
// redraw
|
|
mjui_update(sect_cur-1, item_cur, ui, state, con);
|
|
|
|
// return if change (otherwise return below)
|
|
if (change) {
|
|
return it_cur;
|
|
}
|
|
}
|
|
|
|
// return editchanged, since there was no other change
|
|
return ui->editchanged;
|
|
|
|
case mjEVENT_RELEASE:
|
|
// selection box change
|
|
if (it_rec && it_rec->type == mjITEM_SELECT) {
|
|
// find and set value
|
|
i = findselect(it_rec, ui, state, con);
|
|
if (i >= 0) {
|
|
*(int*)it_rec->pdata = i;
|
|
}
|
|
|
|
// free mouse and redraw ALL (selection box spills over)
|
|
ui->mousesect = 0;
|
|
mjui_update(-1, -1, ui, state, con);
|
|
|
|
// return if value set change
|
|
if (i >= 0) {
|
|
return it_rec;
|
|
}
|
|
}
|
|
|
|
// button release
|
|
else if (it_rec && it_rec->type == mjITEM_BUTTON) {
|
|
// free mouse and redraw
|
|
ui->mousesect = 0;
|
|
mjui_update(sect_rec-1, item_rec, ui, state, con);
|
|
}
|
|
|
|
// free mouse, return no change
|
|
ui->mousesect = 0;
|
|
return NULL;
|
|
|
|
case mjEVENT_SCROLL:
|
|
// adjust scroll
|
|
ui->scroll -= mju_round(state->sy * SCL(ui->spacing.linescroll, con));
|
|
|
|
// clamp scroll
|
|
ui->scroll = mjMAX(0, mjMIN(ui->scroll,
|
|
ui->height-state->rect[ui->rectid].height));
|
|
return NULL;
|
|
|
|
case mjEVENT_KEY:
|
|
// editing
|
|
if (sect_edit > 0) {
|
|
// copy key and clear
|
|
key = state->key;
|
|
state->key = 0;
|
|
|
|
// process key
|
|
switch (key) {
|
|
case mjKEY_ESCAPE: // abandon
|
|
ui->editsect = 0;
|
|
break;
|
|
|
|
case mjKEY_ENTER: // instantiate
|
|
if (text2array(ui->edittext, it_edit) == 0) {
|
|
ui->editsect = 0;
|
|
|
|
// redraw
|
|
mjui_update(sect_edit-1, item_edit, ui, state, con);
|
|
|
|
// record editchanged and return
|
|
ui->editchanged = it_edit;
|
|
return it_edit;
|
|
}
|
|
break;
|
|
|
|
case mjKEY_LEFT: // move cursor left
|
|
// control: emulate Home
|
|
if (state->control) {
|
|
ui->editcursor = 0;
|
|
} else {
|
|
ui->editcursor = mjMAX(0, ui->editcursor-1);
|
|
}
|
|
break;
|
|
|
|
case mjKEY_RIGHT: // move cursor left
|
|
// control: emulate End
|
|
if (state->control) {
|
|
ui->editcursor = strlen(ui->edittext);
|
|
} else {
|
|
ui->editcursor = mjMIN(strlen(ui->edittext), ui->editcursor+1);
|
|
}
|
|
break;
|
|
|
|
case mjKEY_HOME: // move cursor to first position
|
|
ui->editcursor = 0;
|
|
break;
|
|
|
|
case mjKEY_END: // move cursor to last position
|
|
ui->editcursor = strlen(ui->edittext);
|
|
break;
|
|
|
|
case mjKEY_BACKSPACE: // delete before cursor
|
|
if (ui->editcursor > 0) {
|
|
// shift chars after cursor to the left
|
|
for (int i=ui->editcursor; i <= strlen(ui->edittext); i++) {
|
|
ui->edittext[i-1] = ui->edittext[i];
|
|
}
|
|
|
|
// move cursor left
|
|
ui->editcursor = ui->editcursor - 1;
|
|
}
|
|
break;
|
|
|
|
case mjKEY_DELETE: // delete after cursor
|
|
if (ui->editcursor < strlen(ui->edittext)) {
|
|
// shift chars after cursor to the left
|
|
for (int i=ui->editcursor; i <= strlen(ui->edittext); i++) {
|
|
ui->edittext[i] = ui->edittext[i+1];
|
|
}
|
|
}
|
|
break;
|
|
|
|
default: // insert valid key at cursor
|
|
key = validkey(key, strlen(ui->edittext), it_edit->type, state);
|
|
if (key) {
|
|
// shift chars after cursor to the right
|
|
for (int i=strlen(ui->edittext); i >= ui->editcursor; i--) {
|
|
ui->edittext[i+1] = ui->edittext[i];
|
|
}
|
|
|
|
// set at cursor
|
|
ui->edittext[ui->editcursor] = key;
|
|
|
|
// move cursor right
|
|
ui->editcursor = ui->editcursor + 1;
|
|
}
|
|
}
|
|
|
|
// reveal cursor if still editing
|
|
if (ui->editsect > 0) {
|
|
revealcursor(it_edit->rect, ui, con);
|
|
}
|
|
|
|
// redraw
|
|
mjui_update(sect_edit-1, item_edit, ui, state, con);
|
|
}
|
|
|
|
// shortcut search
|
|
else {
|
|
// search section shortcuts
|
|
for (int n=0; n < ui->nsect; n++)
|
|
if (matchshortcut(state, ui->sect[n].modifier, ui->sect[n].shortcut)) {
|
|
// collapse all
|
|
for (int i=0; i < ui->nsect; i++) {
|
|
if (ui->sect[i].state < 2) {
|
|
ui->sect[i].state = 0;
|
|
}
|
|
}
|
|
|
|
// expand this
|
|
if (ui->sect[n].state < 2) {
|
|
ui->sect[n].state = 1;
|
|
}
|
|
|
|
// size and update all
|
|
mjui_resize(ui, con);
|
|
mjui_update(-1, -1, ui, state, con);
|
|
|
|
// clear key
|
|
state->key = 0;
|
|
return NULL;
|
|
}
|
|
|
|
// search item shortcuts
|
|
for (int n=0; n < ui->nsect; n++) {
|
|
for (int i=0; i < ui->sect[n].nitem; i++) {
|
|
// get pointer to item
|
|
it = ui->sect[n].item + i;;
|
|
|
|
// check
|
|
if ((it->type == mjITEM_BUTTON || it->type == mjITEM_CHECKINT ||
|
|
it->type == mjITEM_CHECKBYTE) &&
|
|
matchshortcut(state, it->single.modifier, it->single.shortcut)) {
|
|
// clear key
|
|
state->key = 0;
|
|
|
|
// active: process shortcut
|
|
if (evalpredicate(it->state, ui->predicate, ui->userdata)) {
|
|
// toggle if check
|
|
if (it->type == mjITEM_CHECKINT) {
|
|
*(int*)it->pdata = 1 - *(int*)it->pdata;
|
|
mjui_update(n, i, ui, state, con);
|
|
} else if (it->type == mjITEM_CHECKBYTE) {
|
|
*(mjtByte*)it->pdata = 1 - *(mjtByte*)it->pdata;
|
|
mjui_update(n, i, ui, state, con);
|
|
}
|
|
|
|
// return item
|
|
return it;
|
|
}
|
|
|
|
// inactive: nothing to do
|
|
else {
|
|
return NULL;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
return NULL;
|
|
|
|
case mjEVENT_RESIZE:
|
|
// clamp scroll
|
|
ui->scroll = mjMAX(0, mjMIN(ui->scroll, ui->height - state->rect[ui->rectid].height));
|
|
return NULL;
|
|
}
|
|
|
|
// should not get here, but just in case
|
|
return NULL;
|
|
}
|
|
|
|
|
|
|
|
// Copy UI image to current buffer.
|
|
void mjui_render(mjUI* ui, const mjuiState* state, const mjrContext* con) {
|
|
// get ui rectangle
|
|
mjrRect rect = state->rect[ui->rectid];
|
|
|
|
// clear entire rectangle
|
|
mjr_rectangle(rect, ui->color.master[0], ui->color.master[1],
|
|
ui->color.master[2], 1);
|
|
|
|
// adjust scroll
|
|
if (ui->scroll > 0 && ui->height-ui->scroll < rect.height) {
|
|
ui->scroll = mjMAX(0, ui->height - rect.height);
|
|
}
|
|
|
|
// blit to current buffer
|
|
mjrRect raux = {0, mjMAX(0, ui->height - ui->scroll - rect.height),
|
|
ui->width, mjMIN(rect.height, ui->height - ui->scroll)};
|
|
mjr_blitAux(ui->auxid, raux, rect.left,
|
|
rect.bottom + mjMAX(0, rect.height - ui->height + ui->scroll), con);
|
|
|
|
// draw scrollbar over blit if needed
|
|
if (ui->height > rect.height) {
|
|
// construct rectangles
|
|
mjrRect bar;
|
|
mjrRect thumb;
|
|
scrollrect(rect, ui, con, &bar, &thumb);
|
|
|
|
// draw
|
|
mjr_rectangle(thumb, ui->color.thumb[0], ui->color.thumb[1],
|
|
ui->color.thumb[2], 1);
|
|
}
|
|
|
|
// draw selection box tracking over blit if needed
|
|
if (ui->mousesect > 0 && ui->mouseitem >= 0) {
|
|
// get item pointer
|
|
const mjuiItem* it = ui->sect[ui->mousesect-1].item + ui->mouseitem;
|
|
|
|
// proceed if select type
|
|
if (it->type == mjITEM_SELECT) {
|
|
// get relevant sizes
|
|
int g_texthor = SCL(ui->spacing.texthor, con);
|
|
int g_textver = SCL(ui->spacing.textver, con);
|
|
int g_itemside = SCL(ui->spacing.itemside, con);
|
|
int cellheight = con->charHeight + 2 * g_textver;
|
|
int offset = mjMAX(0, rect.height - ui->height + ui->scroll) -
|
|
mjMAX(0, ui->height - ui->scroll - rect.height);
|
|
|
|
// margin
|
|
mjrRect r = it->rect;
|
|
r.left -= g_itemside;
|
|
r.width += 2*g_itemside;
|
|
r.height = it->multi.nelem * cellheight + g_itemside;
|
|
r.bottom -= r.height;
|
|
r.bottom += offset;
|
|
r.left += rect.left;
|
|
mjr_rectangle(r, ui->color.sectpane[0],
|
|
ui->color.sectpane[1], ui->color.sectpane[2], 1);
|
|
|
|
// box
|
|
r = it->rect;
|
|
r.height = it->multi.nelem * cellheight;
|
|
r.bottom -= r.height;
|
|
r.bottom += offset;
|
|
r.left += rect.left;
|
|
mjr_rectangle(r, ui->color.select2[0],
|
|
ui->color.select2[1], ui->color.select2[2], 1);
|
|
|
|
// hightlight row under mouse
|
|
int k = findselect(it, ui, state, con);
|
|
if (k >= 0) {
|
|
mjrRect r1 = r;
|
|
r1.bottom = r.bottom + (it->multi.nelem-1-k)*cellheight;
|
|
r1.height = cellheight;
|
|
mjr_rectangle(r1, ui->color.select[0],
|
|
ui->color.select[1], ui->color.select[2], 1);
|
|
}
|
|
|
|
// text values
|
|
initOpenGL(&rect, con);
|
|
for (int k=0; k < it->multi.nelem; k++) {
|
|
drawtext(it->multi.name[k],
|
|
r.left+g_texthor - rect.left,
|
|
r.bottom+g_textver+(it->multi.nelem-1-k)*cellheight,
|
|
r.width-2*g_texthor, ui->color.fontactive, con);
|
|
}
|
|
}
|
|
}
|
|
}
|