From ee6a1dba286ab4b548d0b9421ec1000f6c91e2ef Mon Sep 17 00:00:00 2001 From: Emo Todorov Date: Mon, 24 Jun 2024 05:30:25 -0700 Subject: [PATCH] UI improvements. Fixes #1274. Copybara import of the project: -- a1297d997d0b3cc624bbd1e704ffb7fbb2c5db9d by Emo Todorov : UI imrovements When OpenGL buffer size is too small to hold entire UI with all sections open, older sections (as determined by latest mouse click) are automatically closed as needed. To test this new functionality, uncomment the test near the top of mjui_resize(). UI items now have userid, which can be used for event handling. It is set through the (new) last field of mjuiDef, called otherint. Sections titles can now have a checkmark - in which case they are rendered differently. Set via mjuiDef.otherint: 0- none (as before), 1- rendering changed but box not shown, 2- box shown. This can be used to label sections of special interest to the application. -- b0405bfb3005586ef1d2656ea9cc43c608225927 by Emo Todorov : more ui improvements Implemented mjPRESERVE state for adding sections and separators, so you don't have to keep track of old states. simulate.cc is now modified to use this new functionality. I also made the separators in the Physics section collapsible. If you don't like it, change them back to state 1. Also added functionality for handling section checkboxes. -- 10f4c49be62a6605a38b7c0c7bfae1629024ba31 by Emo Todorov : even more ui improvements Selection boxes that extend below the current UI height are finally rendered in full. Sections and collapsible separators now have rounded corners. The radius can be controlled from theme spacing. More theme color settings were added. In particular, color gradients can now be disabled (the two end colors are set independently). -- 96c2dc601a25d31c93b21f8e089d349e729e829e by Emo Todorov : final ui improvements Cleaned up the code a bit. Added more control for color gradients. -- c9eddb679f36df3f0745d37e3fa90c4e66ee6b98 by Emo Todorov : bug fix Fixed some rendering bugs in the new functionality. COPYBARA_INTEGRATE_REVIEW=https://github.com/google-deepmind/mujoco/pull/1749 from emotodorov:ui_improve ddfc21dbcf0482b8f0d5eacab21847c302f71440 PiperOrigin-RevId: 646060325 Change-Id: Idb27aae7cd0311d00f26387bcadf5891175e867d --- doc/APIreference/APItypes.rst | 12 + doc/includes/references.h | 23 +- include/mujoco/mjui.h | 26 +- introspect/enums.py | 10 + introspect/structs.py | 87 +- mjx/requirements.txt | 3 +- simulate/simulate.cc | 108 ++- simulate/simulate.h | 6 +- src/ui/ui_main.c | 1088 +++++++++++++++++--------- unity/Runtime/Bindings/MjBindings.cs | 18 + 10 files changed, 946 insertions(+), 435 deletions(-) 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)]