diff --git a/doc/APIreference/APItypes.rst b/doc/APIreference/APItypes.rst index e5ff73ff..db4c797e 100644 --- a/doc/APIreference/APItypes.rst +++ b/doc/APIreference/APItypes.rst @@ -582,6 +582,18 @@ Item types used in the UI framework. .. mujoco-include:: mjtItem + +.. _mjtSection: + +mjtSection +~~~~~~~~~~ + +State of a UI section. + +.. mujoco-include:: mjtSection + + + .. _tySpecEnums: Spec diff --git a/doc/includes/references.h b/doc/includes/references.h index 0f71cf53..d951fa75 100644 --- a/doc/includes/references.h +++ b/doc/includes/references.h @@ -2286,6 +2286,11 @@ typedef enum mjtItem_ { // UI item type mjNITEM // number of item types } mjtItem; +typedef enum mjtSection_ { // UI section state + mjSECT_CLOSED = 0, // closed state (regular section) + mjSECT_OPEN, // open state (regular section) + mjSECT_FIXED // fixed section: always open, no title +} mjtSection; struct mjuiState_ { // mouse and keyboard state // constants set by user int nrect; // number of rectangles used @@ -2333,6 +2338,8 @@ struct mjuiThemeSpacing_ { // UI visualization theme spacing int scroll; // scrollbar width int label; // label width int section; // section gap + int cornersect; // corner radius for section + int cornersep; // corner radius for separator int itemside; // item side gap int itemmid; // item middle gap int itemver; // item vertical gap @@ -2346,9 +2353,14 @@ struct mjuiThemeColor_ { // UI visualization theme color float master[3]; // master background float thumb[3]; // scrollbar thumb float secttitle[3]; // section title + float secttitle2[3]; // section title: bottom color + float secttitlecheck[3]; // section title with checkbox + float secttitlecheck2[3]; // section title with checkbox: bottom color float sectfont[3]; // section font float sectsymbol[3]; // section symbol float sectpane[3]; // section pane + float separator[3]; // separator title + float separator2[3]; // separator title: bottom color float shortcut[3]; // shortcut background float fontactive[3]; // font active float fontinactive[3]; // font inactive @@ -2398,6 +2410,7 @@ struct mjuiItem_ { // UI item void *pdata; // data pointer (type-specific) int sectionid; // id of section containing item int itemid; // id of item within section + int userid; // user-supplied id (for event handling) // type-specific properties union { @@ -2409,20 +2422,23 @@ struct mjuiItem_ { // UI item // internal mjrRect rect; // rectangle occupied by item + int skip; // item skipped due to closed separator }; typedef struct mjuiItem_ mjuiItem; struct mjuiSection_ { // UI section // properties char name[mjMAXUINAME]; // name - int state; // 0: closed, 1: open + int state; // section state (mjtSection) int modifier; // 0: none, 1: control, 2: shift; 4: alt int shortcut; // shortcut key; 0: undefined + int checkbox; // 0: none, 1: hidden, 2: unchecked, 2: checked int nitem; // number of items in use mjuiItem item[mjMAXUIITEM]; // preallocated array of items // internal mjrRect rtitle; // rectangle occupied by title mjrRect rcontent; // rectangle occupied by content + int lastclick; // last mouse click over this section }; typedef struct mjuiSection_ mjuiSection; struct mjUI_ { // entire UI @@ -2441,10 +2457,12 @@ struct mjUI_ { // entire UI int maxheight; // height when all sections open int scroll; // scroll from top of UI - // mouse focus + // mouse focus and count int mousesect; // 0: none, -1: scroll, otherwise 1+section int mouseitem; // item within section int mousehelp; // help button down: print shortcuts + int mouseclicks; // number of mouse clicks over UI + int mousesectcheck; // 0: none, otherwise 1+section // keyboard focus and edit int editsect; // 0: none, otherwise 1+section @@ -2465,6 +2483,7 @@ struct mjuiDef_ { // table passed to mjui_add() int state; // state void* pdata; // pointer to data char other[mjMAXUITEXT]; // string with type-specific properties + int otherint; // int with type-specific properties }; typedef struct mjuiDef_ mjuiDef; typedef enum mjtCatBit_ { // bitflags for mjvGeom category diff --git a/include/mujoco/mjui.h b/include/mujoco/mjui.h index c6d4c34d..7981d936 100644 --- a/include/mujoco/mjui.h +++ b/include/mujoco/mjui.h @@ -26,6 +26,7 @@ #define mjMAXUIRECT 25 // maximum number of rectangles #define mjSEPCLOSED 1000 // closed state of adjustable separator +#define mjPRESERVE 2000 // preserve section or separator state // key codes matching GLFW (user must remap for other frameworks) @@ -106,6 +107,13 @@ typedef enum mjtItem_ { // UI item type } mjtItem; +typedef enum mjtSection_ { // UI section state + mjSECT_CLOSED = 0, // closed state (regular section) + mjSECT_OPEN, // open state (regular section) + mjSECT_FIXED // fixed section: always open, no title +} mjtSection; + + // predicate function: set enable/disable based on item category typedef int (*mjfItemEnable)(int category, void* data); @@ -163,6 +171,8 @@ struct mjuiThemeSpacing_ { // UI visualization theme spacing int scroll; // scrollbar width int label; // label width int section; // section gap + int cornersect; // corner radius for section + int cornersep; // corner radius for separator int itemside; // item side gap int itemmid; // item middle gap int itemver; // item vertical gap @@ -180,9 +190,14 @@ struct mjuiThemeColor_ { // UI visualization theme color float master[3]; // master background float thumb[3]; // scrollbar thumb float secttitle[3]; // section title + float secttitle2[3]; // section title: bottom color + float secttitlecheck[3]; // section title with checkbox + float secttitlecheck2[3]; // section title with checkbox: bottom color float sectfont[3]; // section font float sectsymbol[3]; // section symbol float sectpane[3]; // section pane + float separator[3]; // separator title + float separator2[3]; // separator title: bottom color float shortcut[3]; // shortcut background float fontactive[3]; // font active float fontinactive[3]; // font inactive @@ -236,6 +251,7 @@ struct mjuiItem_ { // UI item void *pdata; // data pointer (type-specific) int sectionid; // id of section containing item int itemid; // id of item within section + int userid; // user-supplied id (for event handling) // type-specific properties union { @@ -247,6 +263,7 @@ struct mjuiItem_ { // UI item // internal mjrRect rect; // rectangle occupied by item + int skip; // item skipped due to closed separator }; typedef struct mjuiItem_ mjuiItem; @@ -256,15 +273,17 @@ typedef struct mjuiItem_ mjuiItem; struct mjuiSection_ { // UI section // properties char name[mjMAXUINAME]; // name - int state; // 0: closed, 1: open + int state; // section state (mjtSection) int modifier; // 0: none, 1: control, 2: shift; 4: alt int shortcut; // shortcut key; 0: undefined + int checkbox; // 0: none, 1: hidden, 2: unchecked, 2: checked int nitem; // number of items in use mjuiItem item[mjMAXUIITEM]; // preallocated array of items // internal mjrRect rtitle; // rectangle occupied by title mjrRect rcontent; // rectangle occupied by content + int lastclick; // last mouse click over this section }; typedef struct mjuiSection_ mjuiSection; @@ -287,10 +306,12 @@ struct mjUI_ { // entire UI int maxheight; // height when all sections open int scroll; // scroll from top of UI - // mouse focus + // mouse focus and count int mousesect; // 0: none, -1: scroll, otherwise 1+section int mouseitem; // item within section int mousehelp; // help button down: print shortcuts + int mouseclicks; // number of mouse clicks over UI + int mousesectcheck; // 0: none, otherwise 1+section // keyboard focus and edit int editsect; // 0: none, otherwise 1+section @@ -315,6 +336,7 @@ struct mjuiDef_ { // table passed to mjui_add() int state; // state void* pdata; // pointer to data char other[mjMAXUITEXT]; // string with type-specific properties + int otherint; // int with type-specific properties }; typedef struct mjuiDef_ mjuiDef; diff --git a/introspect/enums.py b/introspect/enums.py index 7d22f36f..0542e17e 100644 --- a/introspect/enums.py +++ b/introspect/enums.py @@ -800,4 +800,14 @@ ENUMS: Mapping[str, EnumDecl] = dict([ ('mjNITEM', 14), ]), )), + ('mjtSection', + EnumDecl( + name='mjtSection', + declname='enum mjtSection_', + values=dict([ + ('mjSECT_CLOSED', 0), + ('mjSECT_OPEN', 1), + ('mjSECT_FIXED', 2), + ]), + )), ]) diff --git a/introspect/structs.py b/introspect/structs.py index b3e92f77..47ad0e6a 100644 --- a/introspect/structs.py +++ b/introspect/structs.py @@ -11350,6 +11350,16 @@ STRUCTS: Mapping[str, StructDecl] = dict([ type=ValueType(name='int'), doc='section gap', ), + StructFieldDecl( + name='cornersect', + type=ValueType(name='int'), + doc='corner radius for section', + ), + StructFieldDecl( + name='cornersep', + type=ValueType(name='int'), + doc='corner radius for separator', + ), StructFieldDecl( name='itemside', type=ValueType(name='int'), @@ -11416,6 +11426,30 @@ STRUCTS: Mapping[str, StructDecl] = dict([ ), doc='section title', ), + StructFieldDecl( + name='secttitle2', + type=ArrayType( + inner_type=ValueType(name='float'), + extents=(3,), + ), + doc='section title: bottom color', + ), + StructFieldDecl( + name='secttitlecheck', + type=ArrayType( + inner_type=ValueType(name='float'), + extents=(3,), + ), + doc='section title with checkbox', + ), + StructFieldDecl( + name='secttitlecheck2', + type=ArrayType( + inner_type=ValueType(name='float'), + extents=(3,), + ), + doc='section title with checkbox: bottom color', + ), StructFieldDecl( name='sectfont', type=ArrayType( @@ -11440,6 +11474,22 @@ STRUCTS: Mapping[str, StructDecl] = dict([ ), doc='section pane', ), + StructFieldDecl( + name='separator', + type=ArrayType( + inner_type=ValueType(name='float'), + extents=(3,), + ), + doc='separator title', + ), + StructFieldDecl( + name='separator2', + type=ArrayType( + inner_type=ValueType(name='float'), + extents=(3,), + ), + doc='separator title: bottom color', + ), StructFieldDecl( name='shortcut', type=ArrayType( @@ -11602,6 +11652,11 @@ STRUCTS: Mapping[str, StructDecl] = dict([ type=ValueType(name='int'), doc='id of item within section', ), + StructFieldDecl( + name='userid', + type=ValueType(name='int'), + doc='user-supplied id (for event handling)', + ), AnonymousUnionDecl( fields=( StructFieldDecl( @@ -11631,6 +11686,11 @@ STRUCTS: Mapping[str, StructDecl] = dict([ type=ValueType(name='mjrRect'), doc='rectangle occupied by item', ), + StructFieldDecl( + name='skip', + type=ValueType(name='int'), + doc='item skipped due to closed separator', + ), ), )), ('mjuiSection', @@ -11649,7 +11709,7 @@ STRUCTS: Mapping[str, StructDecl] = dict([ StructFieldDecl( name='state', type=ValueType(name='int'), - doc='0: closed, 1: open', + doc='section state (mjtSection)', ), StructFieldDecl( name='modifier', @@ -11661,6 +11721,11 @@ STRUCTS: Mapping[str, StructDecl] = dict([ type=ValueType(name='int'), doc='shortcut key; 0: undefined', ), + StructFieldDecl( + name='checkbox', + type=ValueType(name='int'), + doc='0: none, 1: hidden, 2: unchecked, 2: checked', + ), StructFieldDecl( name='nitem', type=ValueType(name='int'), @@ -11684,6 +11749,11 @@ STRUCTS: Mapping[str, StructDecl] = dict([ type=ValueType(name='mjrRect'), doc='rectangle occupied by content', ), + StructFieldDecl( + name='lastclick', + type=ValueType(name='int'), + doc='last mouse click over this section', + ), ), )), ('mjUI', @@ -11763,6 +11833,16 @@ STRUCTS: Mapping[str, StructDecl] = dict([ type=ValueType(name='int'), doc='help button down: print shortcuts', ), + StructFieldDecl( + name='mouseclicks', + type=ValueType(name='int'), + doc='number of mouse clicks over UI', + ), + StructFieldDecl( + name='mousesectcheck', + type=ValueType(name='int'), + doc='0: none, otherwise 1+section', + ), StructFieldDecl( name='editsect', type=ValueType(name='int'), @@ -11851,6 +11931,11 @@ STRUCTS: Mapping[str, StructDecl] = dict([ ), doc='string with type-specific properties', ), + StructFieldDecl( + name='otherint', + type=ValueType(name='int'), + doc='int with type-specific properties', + ), ), )), ]) diff --git a/mjx/requirements.txt b/mjx/requirements.txt index 49434299..927cd134 100644 --- a/mjx/requirements.txt +++ b/mjx/requirements.txt @@ -35,7 +35,8 @@ jaxlib==0.4.18; python_version >= '3.9' \ --hash=sha256:9593ff69f424947567e206f3e356b2a2df55ca68e6d815d5adc6cae308e8f652 \ --hash=sha256:2b17b3f05b3bbf8e0ddb85fba339525ac03bac21c9f26d0f83dcea1b1654353e \ --hash=sha256:b35ec08984e2aa5e96ba3f3f8b88e90dee0283649e037f213dec8e85638fa17d \ - --hash=sha256:0bcc4768d29be80d20fd542aafd3a02510a5b1e47c7953beef8b03a9941fa64d + --hash=sha256:0bcc4768d29be80d20fd542aafd3a02510a5b1e47c7953beef8b03a9941fa64d \ + --hash=sha256:1e4b0a0d2cfad3905123af2aa0afc2f6fcfaa9a2f74eb247cd5f140645817ab2 pip==23.3.1 \ --hash=sha256:55eb67bb6171d37447e82213be585b75fe2b12b359e993773aca4de9247a052b pytest==7.4.2 \ diff --git a/simulate/simulate.cc b/simulate/simulate.cc index a424e310..3dc3d239 100644 --- a/simulate/simulate.cc +++ b/simulate/simulate.cc @@ -117,13 +117,13 @@ enum { // file section of UI const mjuiDef defFile[] = { - {mjITEM_SECTION, "File", 1, nullptr, "AF"}, - {mjITEM_BUTTON, "Save xml", 2, nullptr, ""}, - {mjITEM_BUTTON, "Save mjb", 2, nullptr, ""}, - {mjITEM_BUTTON, "Print model", 2, nullptr, "CM"}, - {mjITEM_BUTTON, "Print data", 2, nullptr, "CD"}, - {mjITEM_BUTTON, "Quit", 1, nullptr, "CQ"}, - {mjITEM_BUTTON, "Screenshot", 2, nullptr, "CP"}, + {mjITEM_SECTION, "File", mjPRESERVE, nullptr, "AF"}, + {mjITEM_BUTTON, "Save xml", 2, nullptr, ""}, + {mjITEM_BUTTON, "Save mjb", 2, nullptr, ""}, + {mjITEM_BUTTON, "Print model", 2, nullptr, "CM"}, + {mjITEM_BUTTON, "Print data", 2, nullptr, "CD"}, + {mjITEM_BUTTON, "Quit", 1, nullptr, "CQ"}, + {mjITEM_BUTTON, "Screenshot", 2, nullptr, "CP"}, {mjITEM_END} }; @@ -674,15 +674,15 @@ void UpdateWatch(mj::Simulate* sim, const mjModel* m, const mjData* d) { //---------------------------------- UI construction ----------------------------------------------- // make physics section of UI -void MakePhysicsSection(mj::Simulate* sim, int oldstate) { +void MakePhysicsSection(mj::Simulate* sim) { mjOption* opt = sim->is_passive_ ? &sim->scnstate_.model.opt : &sim->m_->opt; mjuiDef defPhysics[] = { - {mjITEM_SECTION, "Physics", oldstate, nullptr, "AP"}, + {mjITEM_SECTION, "Physics", mjPRESERVE, nullptr, "AP"}, {mjITEM_SELECT, "Integrator", 2, &(opt->integrator), "Euler\nRK4\nimplicit\nimplicitfast"}, {mjITEM_SELECT, "Cone", 2, &(opt->cone), "Pyramidal\nElliptic"}, {mjITEM_SELECT, "Jacobian", 2, &(opt->jacobian), "Dense\nSparse\nAuto"}, {mjITEM_SELECT, "Solver", 2, &(opt->solver), "PGS\nCG\nNewton"}, - {mjITEM_SEPARATOR, "Algorithmic Parameters", 1}, + {mjITEM_SEPARATOR, "Algorithmic Parameters", mjPRESERVE}, {mjITEM_EDITNUM, "Timestep", 2, &(opt->timestep), "1 0 1"}, {mjITEM_EDITINT, "Iterations", 2, &(opt->iterations), "1 0 1000"}, {mjITEM_EDITNUM, "Tolerance", 2, &(opt->tolerance), "1 0 1"}, @@ -695,22 +695,22 @@ void MakePhysicsSection(mj::Simulate* sim, int oldstate) { {mjITEM_EDITNUM, "API Rate", 2, &(opt->apirate), "1 0 1000"}, {mjITEM_EDITINT, "SDF Iter", 2, &(opt->sdf_iterations), "1 1 20"}, {mjITEM_EDITINT, "SDF Init", 2, &(opt->sdf_initpoints), "1 1 100"}, - {mjITEM_SEPARATOR, "Physical Parameters", 1}, + {mjITEM_SEPARATOR, "Physical Parameters", mjPRESERVE}, {mjITEM_EDITNUM, "Gravity", 2, opt->gravity, "3"}, {mjITEM_EDITNUM, "Wind", 2, opt->wind, "3"}, {mjITEM_EDITNUM, "Magnetic", 2, opt->magnetic, "3"}, {mjITEM_EDITNUM, "Density", 2, &(opt->density), "1"}, {mjITEM_EDITNUM, "Viscosity", 2, &(opt->viscosity), "1"}, {mjITEM_EDITNUM, "Imp Ratio", 2, &(opt->impratio), "1"}, - {mjITEM_SEPARATOR, "Disable Flags", 1}, + {mjITEM_SEPARATOR, "Disable Flags", mjPRESERVE}, {mjITEM_END} }; mjuiDef defEnableFlags[] = { - {mjITEM_SEPARATOR, "Enable Flags", 1}, + {mjITEM_SEPARATOR, "Enable Flags", mjPRESERVE}, {mjITEM_END} }; mjuiDef defOverride[] = { - {mjITEM_SEPARATOR, "Contact Override", 1}, + {mjITEM_SEPARATOR, "Contact Override", mjPRESERVE}, {mjITEM_EDITNUM, "Margin", 2, &(opt->o_margin), "1"}, {mjITEM_EDITNUM, "Sol Imp", 2, &(opt->o_solimp), "5"}, {mjITEM_EDITNUM, "Sol Ref", 2, &(opt->o_solref), "2"}, @@ -718,7 +718,7 @@ void MakePhysicsSection(mj::Simulate* sim, int oldstate) { {mjITEM_END} }; mjuiDef defDisableActuator[] = { - {mjITEM_SEPARATOR, "Actuator Group Enable", 1}, + {mjITEM_SEPARATOR, "Actuator Group Enable", mjPRESERVE}, {mjITEM_CHECKBYTE, "Act Group 0", 2, sim->enableactuator+0, ""}, {mjITEM_CHECKBYTE, "Act Group 1", 2, sim->enableactuator+1, ""}, {mjITEM_CHECKBYTE, "Act Group 2", 2, sim->enableactuator+2, ""}, @@ -757,12 +757,12 @@ void MakePhysicsSection(mj::Simulate* sim, int oldstate) { // make rendering section of UI -void MakeRenderingSection(mj::Simulate* sim, const mjModel* m, int oldstate) { +void MakeRenderingSection(mj::Simulate* sim, const mjModel* m) { mjuiDef defRendering[] = { { mjITEM_SECTION, "Rendering", - oldstate, + mjPRESERVE, nullptr, "AR" }, @@ -876,18 +876,18 @@ void MakeRenderingSection(mj::Simulate* sim, const mjModel* m, int oldstate) { } // make visualization section of UI -void MakeVisualizationSection(mj::Simulate* sim, const mjModel* m, int oldstate) { +void MakeVisualizationSection(mj::Simulate* sim, const mjModel* m) { mjStatistic* stat = sim->is_passive_ ? &sim->scnstate_.model.stat : &sim->m_->stat; mjVisual* vis = sim->is_passive_ ? &sim->scnstate_.model.vis : &sim->m_->vis; mjuiDef defVisualization[] = { - {mjITEM_SECTION, "Visualization", oldstate, nullptr, "AV"}, + {mjITEM_SECTION, "Visualization", mjPRESERVE, nullptr, "AV"}, {mjITEM_SEPARATOR, "Headlight", 1}, {mjITEM_RADIO, "Active", 5, &(vis->headlight.active), "Off\nOn"}, {mjITEM_EDITFLOAT, "Ambient", 2, &(vis->headlight.ambient), "3"}, {mjITEM_EDITFLOAT, "Diffuse", 2, &(vis->headlight.diffuse), "3"}, {mjITEM_EDITFLOAT, "Specular", 2, &(vis->headlight.specular), "3"}, - {mjITEM_SEPARATOR, "Free Camera", 1}, + {mjITEM_SEPARATOR, "Free Camera", 1}, {mjITEM_RADIO, "Orthographic", 2, &(vis->global.orthographic), "No\nYes"}, {mjITEM_EDITFLOAT, "Field of view", 2, &(vis->global.fovy), "1"}, {mjITEM_EDITNUM, "Center", 2, &(stat->center), "3"}, @@ -937,9 +937,9 @@ void MakeVisualizationSection(mj::Simulate* sim, const mjModel* m, int oldstate) } // make group section of UI -void MakeGroupSection(mj::Simulate* sim, int oldstate) { +void MakeGroupSection(mj::Simulate* sim) { mjuiDef defGroup[] = { - {mjITEM_SECTION, "Group enable", oldstate, nullptr, "AG"}, + {mjITEM_SECTION, "Group enable", mjPRESERVE, nullptr, "AG"}, {mjITEM_SEPARATOR, "Geom groups", 1}, {mjITEM_CHECKBYTE, "Geom 0", 2, sim->opt.geomgroup, " 0"}, {mjITEM_CHECKBYTE, "Geom 1", 2, sim->opt.geomgroup+1, " 1"}, @@ -997,9 +997,9 @@ void MakeGroupSection(mj::Simulate* sim, int oldstate) { } // make joint section of UI -void MakeJointSection(mj::Simulate* sim, int oldstate) { +void MakeJointSection(mj::Simulate* sim) { mjuiDef defJoint[] = { - {mjITEM_SECTION, "Joint", oldstate, nullptr, "AJ"}, + {mjITEM_SECTION, "Joint", mjPRESERVE, nullptr, "AJ"}, {mjITEM_END} }; mjuiDef defSlider[] = { @@ -1050,9 +1050,9 @@ void MakeJointSection(mj::Simulate* sim, int oldstate) { } // make control section of UI -void MakeControlSection(mj::Simulate* sim, int oldstate) { +void MakeControlSection(mj::Simulate* sim) { mjuiDef defControl[] = { - {mjITEM_SECTION, "Control", oldstate, nullptr, "AC"}, + {mjITEM_SECTION, "Control", mjPRESERVE, nullptr, "AC"}, {mjITEM_BUTTON, "Clear all", 2}, {mjITEM_END} }; @@ -1107,35 +1107,17 @@ void MakeControlSection(mj::Simulate* sim, int oldstate) { // make model-dependent UI sections void MakeUiSections(mj::Simulate* sim, const mjModel* m, const mjData* d) { - // get section open-close state, UI 0 - int oldstate0[NSECT0]; - for (int i=0; iui0.nsect>i) { - oldstate0[i] = sim->ui0.sect[i].state; - } - } - - // get section open-close state, UI 1 - int oldstate1[NSECT1]; - for (int i=0; iui1.nsect>i) { - oldstate1[i] = sim->ui1.sect[i].state; - } - } - // clear model-dependent sections of UI sim->ui0.nsect = SECT_PHYSICS; sim->ui1.nsect = 0; // make - MakePhysicsSection(sim, oldstate0[SECT_PHYSICS]); - MakeRenderingSection(sim, m, oldstate0[SECT_RENDERING]); - MakeVisualizationSection(sim, m, oldstate0[SECT_VISUALIZATION]); - MakeGroupSection(sim, oldstate0[SECT_GROUP]); - MakeJointSection(sim, oldstate1[SECT_JOINT]); - MakeControlSection(sim, oldstate1[SECT_CONTROL]); + MakePhysicsSection(sim); + MakeRenderingSection(sim, m); + MakeVisualizationSection(sim, m); + MakeGroupSection(sim); + MakeJointSection(sim); + MakeControlSection(sim); } //---------------------------------- utility functions --------------------------------------------- @@ -1265,7 +1247,7 @@ int ComputeFontScale(const mj::PlatformUIAdapter& platform_ui) { fs = 150; } fs = mju_round(fs * 0.02) * 50; - fs = mjMIN(250, mjMAX(100, fs)); + fs = mjMIN(300, mjMAX(100, fs)); return fs; } @@ -1322,9 +1304,22 @@ void UiLayout(mjuiState* state) { rect[3].height = rect[0].height; } +// modify UI void UiModify(mjUI* ui, mjuiState* state, mjrContext* con) { mjui_resize(ui, con); - mjr_addAux(ui->auxid, ui->width, ui->maxheight, ui->spacing.samples, con); + + // remake aux buffer only if missing or different + int id = ui->auxid; + if (con->auxFBO[id] == 0 || + con->auxFBO_r[id] == 0 || + con->auxColor[id] == 0 || + con->auxColor_r[id] == 0 || + con->auxWidth[id] != ui->width || + con->auxHeight[id] != ui->maxheight || + con->auxSamples[id] != ui->spacing.samples) { + mjr_addAux(id, ui->width, ui->maxheight, ui->spacing.samples, con); + } + UiLayout(state); mjui_update(-1, -1, ui, state, con); } @@ -1504,7 +1499,7 @@ void UiEvent(mjuiState* state) { // remake joint section if joint group changed if (it->name[0]=='J' && it->name[1]=='o') { sim->ui1.nsect = SECT_JOINT; - MakeJointSection(sim, sim->ui1.sect[SECT_JOINT].state); + MakeJointSection(sim); sim->ui1.nsect = NSECT1; UiModify(&sim->ui1, state, &sim->platform_ui->mjr_context()); } @@ -2457,7 +2452,7 @@ void Simulate::Render() { if (pending_.ui_remake_ctrl) { if (this->ui1_enable && this->ui1.sect[SECT_CONTROL].state) { this->ui1.nsect = SECT_CONTROL; - MakeControlSection(this, this->ui1.sect[SECT_CONTROL].state); + MakeControlSection(this); this->ui1.nsect = NSECT1; UiModify(&this->ui1, &this->uistate, &this->platform_ui->mjr_context()); } @@ -2645,12 +2640,15 @@ void Simulate::RenderLoop() { this->platform_ui->SetEventCallback(UiEvent); this->platform_ui->SetLayoutCallback(UiLayout); - // populate uis with standard sections + // populate uis with standard sections, open some sections initially this->ui0.userdata = this; this->ui1.userdata = this; mjui_add(&this->ui0, defFile); mjui_add(&this->ui0, this->def_option); mjui_add(&this->ui0, this->def_simulation); + this->ui0.sect[0].state = 1; + this->ui0.sect[1].state = 1; + this->ui0.sect[2].state = 1; mjui_add(&this->ui0, this->def_watch); UiModify(&this->ui0, &this->uistate, &this->platform_ui->mjr_context()); UiModify(&this->ui1, &this->uistate, &this->platform_ui->mjr_context()); diff --git a/simulate/simulate.h b/simulate/simulate.h index 5c385a74..bffacb37 100644 --- a/simulate/simulate.h +++ b/simulate/simulate.h @@ -265,7 +265,7 @@ class Simulate { // Constant arrays needed for the option section of UI and the UI interface // TODO setting the size here is not ideal const mjuiDef def_option[13] = { - {mjITEM_SECTION, "Option", 1, nullptr, "AO"}, + {mjITEM_SECTION, "Option", mjPRESERVE, nullptr, "AO"}, {mjITEM_CHECKINT, "Help", 2, &this->help, " #290"}, {mjITEM_CHECKINT, "Info", 2, &this->info, " #291"}, {mjITEM_CHECKINT, "Profiler", 2, &this->profiler, " #292"}, @@ -287,7 +287,7 @@ class Simulate { // simulation section of UI const mjuiDef def_simulation[14] = { - {mjITEM_SECTION, "Simulation", 1, nullptr, "AS"}, + {mjITEM_SECTION, "Simulation", mjPRESERVE, nullptr, "AS"}, {mjITEM_RADIO, "", 5, &this->run, "Pause\nRun"}, {mjITEM_BUTTON, "Reset", 2, nullptr, " #259"}, {mjITEM_BUTTON, "Reload", 5, nullptr, "CL"}, @@ -306,7 +306,7 @@ class Simulate { // watch section of UI const mjuiDef def_watch[5] = { - {mjITEM_SECTION, "Watch", 0, nullptr, "AW"}, + {mjITEM_SECTION, "Watch", mjPRESERVE, nullptr, "AW"}, {mjITEM_EDITTXT, "Field", 2, this->field, "qpos"}, {mjITEM_EDITINT, "Index", 2, &this->index, "1"}, {mjITEM_STATIC, "Value", 2, nullptr, " "}, diff --git a/src/ui/ui_main.c b/src/ui/ui_main.c index 523d9cca..6ccf1421 100644 --- a/src/ui/ui_main.c +++ b/src/ui/ui_main.c @@ -14,6 +14,7 @@ #include "ui/ui_main.h" +#include #include #include @@ -26,142 +27,165 @@ // 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; + 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, // 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; + 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}, // 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]; + {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}, // 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]; + {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}, // 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]; + {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}, // 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]; + {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 @@ -172,7 +196,7 @@ static int SCL(int sz, const mjrContext* con) { // init OpenGL -static void initOpenGL(const mjUI* ui, const mjrContext* con) { +static void initOpenGL(const mjrRect* r, const mjrContext* con) { // set OpenGL options glDisable(GL_NORMALIZE); glDisable(GL_DEPTH_TEST); @@ -186,12 +210,12 @@ static void initOpenGL(const mjUI* ui, const mjrContext* con) { // standard 2D projection, in framebuffer units glMatrixMode(GL_PROJECTION); glLoadIdentity(); - glOrtho(0, ui->width, 0, ui->height, -1, 1); + glOrtho(0, r->width, 0, r->height, -1, 1); glMatrixMode(GL_MODELVIEW); glLoadIdentity(); // set viewport - glViewport(0, 0, ui->width, ui->height); + glViewport(r->left, r->bottom, r->width, r->height); } @@ -277,10 +301,72 @@ static void drawrectangle(mjrRect rect, const float* rgb, const float* rgbback, +// 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 = 20; + const int ndivide = 15; // require horizontal if (rect.height > rect.width) { @@ -334,8 +420,9 @@ static void drawoval(mjrRect rect, const float* rgb, const float* rgbback, -// draw section open/closed symbol: section -static void drawsymbol(mjrRect rect, int flg_open, int flg_sep, +// 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); @@ -344,7 +431,7 @@ static void drawsymbol(mjrRect rect, int flg_open, int flg_sep, int d = mju_round(con->charHeight*0.33); // separator size - if (flg_sep) { + if (type == 2) { d = mju_round(con->charHeight*0.28); } @@ -360,7 +447,7 @@ static void drawsymbol(mjrRect rect, int flg_open, int flg_sep, // closed else { - // solid + // solid outside glColor3fv(ui->color.sectsymbol); glBegin(GL_TRIANGLES); glVertex2i(cx, cy-d); @@ -368,23 +455,36 @@ static void drawsymbol(mjrRect rect, int flg_open, int flg_sep, 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) { + // set color for inside + switch (type) { + case 0: // section 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 { + (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.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 - ); + (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); @@ -636,6 +736,7 @@ static int insideoval(int x, int y, mjrRect r) { // 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 @@ -689,6 +790,16 @@ static void findmouse(const mjUI* ui, const mjuiState* ins, const mjrContext* co 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; } @@ -1152,18 +1263,32 @@ void mjui_add(mjUI* ui, const mjuiDef* def) { if (strlen(def[n].name) >= mjMAXUINAME-1) { mju_error("mjui_add: section name too long"); } - if (def[n].state < 0 || def[n].state > 2) { + 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, clear + // add section, save state ui->nsect++; mjuiSection* se = ui->sect + (ui->nsect-1); - memset(se, 0, sizeof(mjuiSection)); + int oldstate = se->state; - // copy data + // 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->state = def[n].state; + se->checkbox = def[n].otherint; parseshortcut(def[n].other, &(se->modifier), &(se->shortcut)); } @@ -1191,18 +1316,28 @@ void mjui_add(mjUI* ui, const mjuiDef* def) { mju_error("mjui_add: invalid item state"); } - // add item, clear + // 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->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; + it->userid = def[n].otherint; // data pointer check if (it->type > mjITEM_BUTTON && it->pdata == 0) { @@ -1343,10 +1478,38 @@ void mjui_addToSection(mjUI* ui, int sect, const mjuiDef* def) { -// Compute UI sizes. -void mjui_resize(mjUI* ui, const mjrContext* con) { +// 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_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); @@ -1361,183 +1524,279 @@ void mjui_resize(mjUI* ui, const mjrContext* con) { // 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; + // 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 - int skip; - for (int n=0; n < ui->nsect; n++) { - // vertical padding before section + // 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; - // 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; + // save data: maxheight from pass 0, height from pass 1 + if (pass == 0) { + MaxHeight = maxheight; } - - // 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; - } + else { + Height = 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) { - 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; + 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; + s->item[i].rect.bottom = Height - s->item[i].rect.bottom; } } // assign UI sizes ui->width = w_master; - ui->height = height; - ui->maxheight = maxheight; + 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"); + } + } } @@ -1653,7 +1912,8 @@ void mjui_update(int section, int item, const mjUI* ui, // start rendering mjr_setAux(ui->auxid, con); - initOpenGL(ui, con); + mjrRect rgl = { 0, 0, ui->width, ui->height }; + initOpenGL(&rgl, con); // all sections: clear background if (section < 0) { @@ -1684,34 +1944,81 @@ void mjui_update(int section, int item, const mjUI* ui, // 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(); + r = s->rtitle; - // 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); + // 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(s->rtitle, s->modifier, s->shortcut, ui, con); + shortcuthelp(r, s->modifier, s->shortcut, ui, con); } } // content pane, active only - if (s->state) { + 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 @@ -1754,10 +2061,10 @@ void mjui_update(int section, int item, const mjUI* ui, // background r = it->rect; glBegin(GL_QUADS); - glColor3fv(ui->color.sectpane); + glColor3fv(ui->color.separator2); glVertex2i(r.left, r.bottom); glVertex2i(r.left+r.width, r.bottom); - glColor3fv(ui->color.master); + glColor3fv(ui->color.separator); glVertex2i(r.left+r.width, r.bottom+r.height); glVertex2i(r.left, r.bottom+r.height); glEnd(); @@ -1768,13 +2075,12 @@ void mjui_update(int section, int item, const mjUI* ui, 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); + // 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: @@ -1945,7 +2251,7 @@ void mjui_update(int section, int item, const mjUI* ui, it->rect.width-2*g_texthor, rgbfont, con); } - // draw tracking at the end + // draw tracking in mjui_render() break; case mjITEM_SLIDERINT: @@ -2062,46 +2368,6 @@ void mjui_update(int section, int item, const mjUI* ui, } } - // 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); } @@ -2114,6 +2380,11 @@ mjuiItem* mjui_event(mjUI* ui, mjuiState* state, const mjrContext* con) { 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) { @@ -2139,6 +2410,11 @@ mjuiItem* mjui_event(mjUI* ui, mjuiState* state, const mjrContext* con) { 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; @@ -2242,19 +2518,33 @@ mjuiItem* mjui_event(mjUI* ui, mjuiState* state, const mjrContext* con) { // section title else if (sect_cur > 0 && item_cur < 0) { - // double-click: make all sections like this + 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 < 2 && ui->sect[sect_cur-1].state < 2) { - ui->sect[i].state = ui->sect[sect_cur-1].state; + if (ui->sect[i].state != mjSECT_FIXED && se->state != mjSECT_FIXED) { + ui->sect[i].state = se->state; } } } - // single click: toggle section state + // single click: toggle section state (exclude fixed) else { - if (ui->sect[sect_cur-1].state < 2) { - ui->sect[sect_cur-1].state = 1 - ui->sect[sect_cur-1].state; + if (se->state == mjSECT_OPEN) { + se->state = mjSECT_CLOSED; + } + else if (se->state == mjSECT_CLOSED) { + se->state = mjSECT_OPEN; } } @@ -2607,7 +2897,7 @@ void mjui_render(mjUI* ui, const mjuiState* state, const mjrContext* con) { mjr_blitAux(ui->auxid, raux, rect.left, rect.bottom + mjMAX(0, rect.height - ui->height + ui->scroll), con); - // draw scrollbar on top if needed + // draw scrollbar over blit if needed if (ui->height > rect.height) { // construct rectangles mjrRect bar; @@ -2618,4 +2908,60 @@ void mjui_render(mjUI* ui, const mjuiState* state, const mjrContext* con) { 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); + } + } + } } diff --git a/unity/Runtime/Bindings/MjBindings.cs b/unity/Runtime/Bindings/MjBindings.cs index c6afdeff..649005f7 100644 --- a/unity/Runtime/Bindings/MjBindings.cs +++ b/unity/Runtime/Bindings/MjBindings.cs @@ -71,6 +71,7 @@ public const int mjMAXUIMULTI = 35; public const int mjMAXUIEDIT = 7; public const int mjMAXUIRECT = 25; public const int mjSEPCLOSED = 1000; +public const int mjPRESERVE = 2000; public const int mjKEY_ESCAPE = 256; public const int mjKEY_ENTER = 257; public const int mjKEY_TAB = 258; @@ -482,6 +483,11 @@ public enum mjtEvent : int{ mjEVENT_REDRAW = 7, mjEVENT_FILESDROP = 8, } +public enum mjtSection : int{ + mjSECT_CLOSED = 0, + mjSECT_OPEN = 1, + mjSECT_FIXED = 2, +} public enum mjtCatBit : int{ mjCAT_STATIC = 1, mjCAT_DYNAMIC = 2, @@ -5722,6 +5728,8 @@ public unsafe struct mjuiThemeSpacing_ { public int scroll; public int label; public int section; + public int cornersect; + public int cornersep; public int itemside; public int itemmid; public int itemver; @@ -5736,9 +5744,14 @@ public unsafe struct mjuiThemeColor_ { public fixed float master[3]; public fixed float thumb[3]; public fixed float secttitle[3]; + public fixed float secttitle2[3]; + public fixed float secttitlecheck[3]; + public fixed float secttitlecheck2[3]; public fixed float sectfont[3]; public fixed float sectsymbol[3]; public fixed float sectpane[3]; + public fixed float separator[3]; + public fixed float separator2[3]; public fixed float shortcut[3]; public fixed float fontactive[3]; public fixed float fontinactive[3]; @@ -5786,9 +5799,11 @@ public unsafe struct mjuiSection_ { public int state; public int modifier; public int shortcut; + public int checkbox; public int nitem; public mjrRect_ rtitle; public mjrRect_ rcontent; + public int lastclick; } [StructLayout(LayoutKind.Sequential)] @@ -5807,6 +5822,8 @@ public unsafe struct mjUI_ { public int mousesect; public int mouseitem; public int mousehelp; + public int mouseclicks; + public int mousesectcheck; public int editsect; public int edititem; public int editcursor; @@ -5833,6 +5850,7 @@ public unsafe struct mjuiDef_ { public int state; public void* pdata; public fixed sbyte other[300]; + public int otherint; } [StructLayout(LayoutKind.Sequential)]