Files
Mujoco_WASM/src/ui/ui_main.c
T
Yuval Tassa d1c6561f1c Add spaces around comparison operators in render/ and ui/ source files.
PiperOrigin-RevId: 536001297
Change-Id: Ia0d97bf9f886aea03b8363e4d97e18301520909f
2023-05-28 07:04:49 -07:00

2622 lines
70 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 <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, // int total;
15, // int scroll;
120, // int label;
8, // int section;
4, // int itemside;
4, // int itemmid;
4, // int itemver;
8, // int texthor;
4, // int textver;
30, // int linescroll;
4 // int samples;
};
// theme spacing 1 : wide
static const mjuiThemeSpacing themeSpacing1 = {
310, // int total;
15, // int scroll;
120, // int label;
10, // int section;
7, // int itemside;
7, // int itemmid;
7, // int itemver;
10, // int texthor;
5, // int textver;
30, // int linescroll;
4 // int samples;
};
// theme color 0 : default
static const mjuiThemeColor themeColor0 = {
{0.25, 0.25, 0.25}, // float master[3];
{0.12, 0.12, 0.12}, // float thumb[3];
{0.6, 0.2, 0.2}, // float secttitle[3];
{1.0, 1.0, 1.0}, // float sectfont[3];
{0.7, 0.7, 0.7}, // float sectsymbol[3];
{0.1, 0.1, 0.1}, // float sectpane[3];
{0.0, 0.0, 1.0}, // float shortcut[3];
{1.0, 1.0, 1.0}, // float fontactive[3];
{0.5, 0.5, 0.5}, // float fontinactive[3];
{0.3, 0.3, 0.3}, // float decorinactive[3];
{0.4, 0.4, 0.4}, // float decorinactive2[3];
{0.6, 0.4, 0.4}, // float button[3];
{0.4, 0.4, 0.7}, // float check[3];
{0.4, 0.6, 0.4}, // float radio[3];
{0.4, 0.6, 0.6}, // float select[3];
{0.2, 0.3, 0.3}, // float select2[3];
{0.3, 0.2, 0.3}, // float slider[3];
{0.6, 0.4, 0.6}, // float slider2[3];
{0.6, 0.6, 0.4}, // float edit[3];
{0.7, 0.0, 0.0}, // float edit2[3];
{0.9, 0.9, 0.9} // float cursor[3];
};
// theme color 1 : orange
static const mjuiThemeColor themeColor1 = {
{0.2, 0.2, 0.2}, // float master[3];
{0.12, 0.12, 0.12}, // float thumb[3];
{0.3, 0.3, 0.3}, // float secttitle[3];
{0.8, 0.8, 0.8}, // float sectfont[3];
{0.7, 0.7, 0.7}, // float sectsymbol[3];
{0.15, 0.15, 0.15}, // float sectpane[3];
{0.0, 0.0, 1.0}, // float shortcut[3];
{0.9, 0.9, 0.9}, // float fontactive[3];
{0.5, 0.5, 0.5}, // float fontinactive[3];
{0.2, 0.2, 0.2}, // float decorinactive[3];
{0.25, 0.25, 0.25}, // float decorinactive2[3];
{0.6, 0.4, 0.2}, // float button[3];
{0.6, 0.4, 0.2}, // float check[3];
{0.6, 0.4, 0.2}, // float radio[3];
{0.6, 0.4, 0.2}, // float select[3];
{0.3, 0.2, 0.1}, // float select2[3];
{0.2, 0.2, 0.2}, // float slider[3];
{0.6, 0.4, 0.2}, // float slider2[3];
{0.6, 0.4, 0.2}, // float edit[3];
{0.7, 0.0, 0.0}, // float edit2[3];
{0.9, 0.9, 0.9} // float cursor[3];
};
// theme color 2 : white
static const mjuiThemeColor themeColor2 = {
{0.9, 0.9, 0.9}, // float master[3];
{0.7, 0.7, 0.7}, // float thumb[3];
{0.8, 0.8, 0.8}, // float secttitle[3];
{0.0, 0.0, 0.8}, // float sectfont[3];
{0.0, 0.0, 0.8}, // float sectsymbol[3];
{1.0, 1.0, 1.0}, // float sectpane[3];
{0.0, 1.0, 1.0}, // float shortcut[3];
{0.0, 0.0, 0.0}, // float fontactive[3];
{0.7, 0.7, 0.7}, // float fontinactive[3];
{0.95, 0.95, 0.95}, // float decorinactive[3];
{0.9, 0.9, 0.9}, // float decorinactive2[3];
{0.8, 0.8, 0.8}, // float button[3];
{0.8, 0.8, 0.8}, // float check[3];
{0.8, 0.8, 0.8}, // float radio[3];
{0.8, 0.8, 0.8}, // float select[3];
{0.9, 0.9, 0.9}, // float select2[3];
{0.95, 0.95, 0.95}, // float slider[3];
{0.8, 0.8, 0.8}, // float slider2[3];
{0.8, 0.8, 0.8}, // float edit[3];
{1.0, 0.3, 0.3}, // float edit2[3];
{0.2, 0.2, 0.2} // float cursor[3];
};
// theme color 3 : black
static const mjuiThemeColor themeColor3 = {
{0.15, 0.15, 0.15}, // float master[3];
{0.3, 0.3, 0.3}, // float thumb[3];
{0.25, 0.25, 0.25}, // float secttitle[3];
{1.0, 0.3, 0.3}, // float sectfont[3];
{1.0, 0.3, 0.3}, // float sectsymbol[3];
{0.0, 0.0, 0.0}, // float sectpane[3];
{0.0, 0.0, 1.0}, // float shortcut[3];
{1.0, 1.0, 1.0}, // float fontactive[3];
{0.4, 0.4, 0.4}, // float fontinactive[3];
{0.1, 0.1, 0.1}, // float decorinactive[3];
{0.15, 0.15, 0.15}, // float decorinactive2[3];
{0.3, 0.3, 0.3}, // float button[3];
{0.3, 0.3, 0.3}, // float check[3];
{0.3, 0.3, 0.3}, // float radio[3];
{0.3, 0.3, 0.3}, // float select[3];
{0.15, 0.15, 0.15}, // float select2[3];
{0.15, 0.15, 0.15}, // float slider[3];
{0.3, 0.3, 0.3}, // float slider2[3];
{0.3, 0.3, 0.3}, // float edit[3];
{0.8, 0.2, 0.2}, // float edit2[3];
{0.8, 0.8, 0.8} // float cursor[3];
};
//------------------------------------ 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 mjUI* ui, 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, ui->width, 0, ui->height, -1, 1);
glMatrixMode(GL_MODELVIEW);
glLoadIdentity();
// set viewport
glViewport(0, 0, ui->width, ui->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();
}
}
// draw oval
static void drawoval(mjrRect rect, const float* rgb, const float* rgbback,
const mjrContext* con) {
const int ndivide = 20;
// 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 section open/closed symbol: section
static void drawsymbol(mjrRect rect, int flg_open, int flg_sep,
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 (flg_sep) {
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
glColor3fv(ui->color.sectsymbol);
glBegin(GL_TRIANGLES);
glVertex2i(cx, cy-d);
glVertex2i(cx, cy+d);
glVertex2i(cx-2*d, cy);
glEnd();
// empty
double margin = con->fontScale * 0.015;
double u = 0.5*sqrt(5.0)*margin;
double y = d - u - 0.5*margin;
if (flg_sep) {
glColor3f(
(ui->color.master[0] + ui->color.sectpane[0]) * 0.5,
(ui->color.master[1] + ui->color.sectpane[1]) * 0.5,
(ui->color.master[2] + ui->color.sectpane[2]) * 0.5
);
} else {
glColor3f(
(ui->color.secttitle[0] + ui->color.sectpane[0]) * 0.5,
(ui->color.secttitle[1] + ui->color.sectpane[1]) * 0.5,
(ui->color.secttitle[2] + ui->color.sectpane[2]) * 0.5
);
}
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
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;
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 < 0 || def[n].state > 2) {
mju_error("mjui_add: invalid section state");
}
// add section, clear
ui->nsect++;
mjuiSection* se = ui->sect + (ui->nsect-1);
memset(se, 0, sizeof(mjuiSection));
// copy data
mjSTRNCPY(se->name, def[n].name);
se->state = def[n].state;
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, clear
se->nitem++;
mjuiItem* it = se->item + (se->nitem-1);
memset(it, 0, sizeof(mjuiItem));
// copy common data
it->type = def[n].type;
it->state = def[n].state;
it->pdata = def[n].pdata;
mjSTRNCPY(it->name, def[n].name);
it->sectionid = ui->nsect - 1;
it->itemid = se->nitem - 1;
// 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;
}
// Compute UI sizes.
void mjui_resize(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;
// init UI sizes
int height = 0;
int maxheight = 0;
// process sections
int skip;
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;
// title rectangle
s->rtitle.left = g_section;
s->rtitle.width = w_master - w_scroll - 2*g_section;
if (s->state < 2) {
s->rtitle.bottom = height + textheight;
s->rtitle.height = textheight;
} else {
s->rtitle.bottom = height;
s->rtitle.height = 0;
}
// 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;
// process items within section
for (int i=0; i < s->nitem; i++) {
// get item pointer
mjuiItem* it = s->item + i;
// save section rcontent
mjrRect oldcontent = s->rcontent;
// determine skip (collapsed separator before item)
skip = 0;
if (i > 0 && it->type != mjITEM_SEPARATOR) {
for (int k=i-1; k >= 0; k--) {
if (s->item[k].type == mjITEM_SEPARATOR) {
// collapsed state: skip items below it
if (s->item[k].state == mjSEPCLOSED) {
skip = 1;
}
break;
}
}
}
// 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;
// skip second item in pair
if (skip) {
it1->rect.width = 0;
it1->rect.height = 0;
}
// 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;
// skip item
if (skip) {
maxheight += s->rcontent.height - oldcontent.height;
s->rcontent = oldcontent;
it->rect.width = 0;
it->rect.height = 0;
}
}
// 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) {
height += s->rcontent.height;
}
maxheight += s->rcontent.height;
}
// vertical padding after last section
height += g_section;
maxheight += g_section;
// 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;
}
// 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);
initOpenGL(ui, 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) {
// title shown
if (s->state < 2) {
// interpolated rectangle
r = s->rtitle;
glBegin(GL_QUADS);
glColor3fv(ui->color.sectpane);
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(s->rtitle, s->state, 0, ui, con);
drawtext(s->name, s->rtitle.left+g_texthor, s->rtitle.bottom+g_textver,
2*maxwidth, ui->color.sectfont, con);
// shortcut
if (ui->mousehelp && s->shortcut) {
shortcuthelp(s->rtitle, s->modifier, s->shortcut, ui, con);
}
}
// content pane, active only
if (s->state) {
drawrectangle(s->rcontent, ui->color.sectpane, NULL, 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.sectpane);
glVertex2i(r.left, r.bottom);
glVertex2i(r.left+r.width, r.bottom);
glColor3fv(ui->color.master);
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
if (it->state == mjSEPCLOSED+1) {
drawsymbol(it->rect, 1, 1, ui, con);
} else if (it->state == mjSEPCLOSED) {
drawsymbol(it->rect, 0, 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 at the end
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");
}
}
}
// select tracking
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) {
// margin
r = it->rect;
r.left -= g_itemside;
r.width += 2*g_itemside;
r.height = it->multi.nelem * cellheight + g_itemside;
r.bottom -= r.height;
drawrectangle(r, ui->color.sectpane, NULL, con);
// box
r = it->rect;
r.height = it->multi.nelem * cellheight;
r.bottom -= r.height;
drawrectangle(r, ui->color.select2, NULL, con);
// 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;
drawrectangle(r1, ui->color.select, NULL, con);
}
// values
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-1-k)*cellheight,
r.width-2*g_texthor, ui->color.fontactive, con);
}
}
}
// 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;
// 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, &sect_cur, &item_cur);
if (sect_cur > 0 && item_cur >= 0) {
it_cur = ui->sect[sect_cur-1].item + item_cur;
}
// 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) {
// double-click: make all sections like this
if (state->doubleclick) {
for (int i=0; i < ui->nsect; i++) {
if (ui->sect[i].state < 2 && ui->sect[sect_cur-1].state < 2) {
ui->sect[i].state = ui->sect[sect_cur-1].state;
}
}
}
// single click: toggle section state
else {
if (ui->sect[sect_cur-1].state < 2) {
ui->sect[sect_cur-1].state = 1 - ui->sect[sect_cur-1].state;
}
}
// 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 on top 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);
}
}