1 /// 2 module nanogui.experimental.treeview; 3 4 /* 5 NanoGUI was developed by Wenzel Jakob <wenzel.jakob@epfl.ch>. 6 The widget drawing code is based on the NanoVG demo application 7 by Mikko Mononen. 8 9 All rights reserved. Use of this source code is governed by a 10 BSD-style license that can be found in the LICENSE.txt file. 11 */ 12 13 import nanogui.widget; 14 import nanogui.common : Vector2i, Vector2f, MouseButton; 15 16 /** 17 * Tree view widget. 18 * 19 * Remarks: 20 * This class overrides `nanogui.Widget.mIconExtraScale` to be `1.2f`, 21 * which affects all subclasses of this Widget. Subclasses must explicitly 22 * set a different value if needed (e.g., in their constructor). 23 */ 24 class TreeView(TreeModel) : Widget 25 { 26 public: 27 /** 28 * Adds a TreeView to the specified `parent`. 29 * 30 * Params: 31 * parent = The Widget to add this TreeView to. 32 * caption = The caption text of the TreeView (default `"Untitled"`). 33 * callback = If provided, the callback to execute when the TreeView is 34 * checked or unchecked. Default parameter function does nothing. See 35 * `nanogui.TreeView.mPushed` for the difference between "pushed" 36 * and "checked". 37 */ 38 this(Widget parent, const string caption, TreeModel model, void delegate(bool) callback) 39 { 40 super(parent); 41 mCaption = caption; 42 mPushed = false; 43 mChecked = false; 44 mCallback = callback; 45 mIconExtraScale = 1.2f;// widget override 46 47 this.model = model; 48 } 49 50 /// The caption of this TreeView. 51 final string caption() const { return mCaption; } 52 53 /// Sets the caption of this TreeView. 54 final void caption(string caption) { mCaption = caption; } 55 56 /// Whether or not this TreeView is currently checked. 57 final bool checked() const { return mChecked; } 58 59 /// Sets whether or not this TreeView is currently checked. 60 final void checked(bool checked) { mChecked = checked; } 61 62 /// Whether or not this TreeView is currently pushed. See `nanogui.TreeView.mPushed`. 63 final bool pushed() const { return mPushed; } 64 65 /// Sets whether or not this TreeView is currently pushed. See `nanogui.TreeView.mPushed`. 66 final void pushed(bool pushed) { mPushed = pushed; } 67 68 /// Returns the current callback of this TreeView. 69 final void delegate(bool) callback() const { return mCallback; } 70 71 /// Sets the callback to be executed when this TreeView is checked / unchecked. 72 final void callback(void delegate(bool) callback) { mCallback = callback; } 73 74 /** 75 * The mouse button callback will return `true` when all three conditions are met: 76 * 77 * 1. This TreeView is "enabled" (see `nanogui.Widget.mEnabled`). 78 * 2. `p` is inside this TreeView. 79 * 3. `button` is `MouseButton.Left`. 80 * 81 * Since a mouse button event is issued for both when the mouse is pressed, as well 82 * as released, this function sets `nanogui.TreeView.mPushed` to `true` when 83 * parameter `down == true`. When the second event (`down == false`) is fired, 84 * `nanogui.TreeView.mChecked` is inverted and `nanogui.TreeView.mCallback` 85 * is called. 86 * 87 * That is, the callback provided is only called when the mouse button is released, 88 * **and** the click location remains within the TreeView boundaries. If the user 89 * clicks on the TreeView and releases away from the bounds of the TreeView, 90 * `nanogui.TreeView.mPushed` is simply set back to `false`. 91 */ 92 override bool mouseButtonEvent(Vector2i p, MouseButton button, bool down, int modifiers) 93 { 94 super.mouseButtonEvent(p, button, down, modifiers); 95 if (!mEnabled) 96 return false; 97 98 import nanogui.experimental.utils : isPointInRect; 99 const rect_size = Vector2i(mSize.x, mChecked ? cast(int) (fontSize() * 1.3f) : mSize.y); 100 101 if (button == MouseButton.Left) 102 { 103 import std.stdio; 104 writeln(tree_path); 105 if (!isPointInRect(mPos, rect_size, p)) 106 return false; 107 if (down) 108 { 109 mPushed = true; 110 } 111 else if (mPushed) 112 { 113 mChecked = !mChecked; 114 if (mCallback) 115 mCallback(mChecked); 116 mPushed = false; 117 } 118 return true; 119 } 120 return false; 121 } 122 123 /// The preferred size of this TreeView. 124 override Vector2i preferredSize(NanoContext ctx) const 125 { 126 if (mFixedSize != Vector2i()) 127 return mFixedSize; 128 ctx.fontSize(fontSize()); 129 ctx.fontFace("sans"); 130 float[4] bounds; 131 const extra = mChecked ? (fontSize() * 1.3f * 1/*model.length*/) : 0; 132 return cast(Vector2i) Vector2f( 133 (ctx.textBounds(0, 0, mCaption, bounds[]) + 134 1.8f * fontSize()), 135 fontSize() * 1.3f + extra); 136 } 137 138 /// Draws this TreeView. 139 override void draw(ref NanoContext ctx) 140 { 141 // do not call super.draw() because we do custom drawing 142 143 Vector2i titleSize = void; 144 titleSize.x = mSize.x; 145 titleSize.y = mChecked ? cast(int) (fontSize() * 1.3f) : mSize.y; 146 147 ctx.save; 148 scope(exit) ctx.restore; 149 { 150 // background for icon 151 NVGPaint bg = ctx.boxGradient(mPos.x + 1.5f, mPos.y + 1.5f, 152 titleSize.y - 2.0f, titleSize.y - 2.0f, 3, 3, 153 mPushed ? Color(0, 0, 0, 100) : Color(0, 0, 0, 32), 154 Color(0, 0, 0, 180)); 155 156 ctx.beginPath; 157 ctx.roundedRect(mPos.x + 1.0f, mPos.y + 1.0f, titleSize.y - 2.0f, 158 titleSize.y - 2.0f, 3); 159 ctx.fillPaint(bg); 160 ctx.fill; 161 } 162 163 ctx.position = mPos; 164 ctx.theme = theme; 165 ctx.current_size = 0; // prevents highlighting of icon 166 const old = ctx.mouse; 167 ctx.mouse -= window.absolutePosition; 168 scope(exit) ctx.mouse = old; 169 170 import nanogui.experimental.utils : drawItem, indent, unindent; 171 { 172 // icon 173 ctx.fontSize(titleSize.y * icon_scale()); 174 ctx.fontFace("icons"); 175 ctx.fillColor(mEnabled ? mTheme.mIconColor 176 : mTheme.mDisabledTextColor); 177 // NVGTextAlign algn; 178 // algn.center = true; 179 // algn.middle = true; 180 // ctx.textAlign(algn); 181 182 import nanogui.entypo : Entypo; 183 drawItem(ctx, titleSize.y, 184 [mChecked ? cast(dchar)Entypo.ICON_CHEVRON_DOWN : 185 cast(dchar)Entypo.ICON_CHEVRON_RIGHT 186 ]); 187 ctx.position -= Vector2i(0, titleSize.y); 188 } 189 190 ctx.current_size = size.x - titleSize.y; 191 192 ctx.tree_view_nesting_level = 0; 193 194 { 195 // Caption 196 ctx.position += Vector2i(cast(int)(1.6f * fontSize), 0); 197 scope(exit) ctx.position -= Vector2i(cast(int)(1.6f * fontSize), 0); 198 ctx.fontSize(fontSize); 199 ctx.fontFace("sans"); 200 ctx.fillColor(mEnabled ? mTheme.mTextColor : mTheme.mDisabledTextColor); 201 tree_path.length = 0; 202 if (drawItem(ctx, titleSize.y, mCaption)) 203 { 204 tree_path.length = 1; 205 tree_path[ctx.tree_view_nesting_level] = 0; 206 } 207 } 208 209 // content of tree view 210 if (mChecked) 211 { 212 ctx.indent; 213 ctx.tree_view_nesting_level++; 214 scope(exit) 215 { 216 assert(ctx.tree_view_nesting_level > 0); 217 ctx.tree_view_nesting_level--; 218 ctx.unindent; 219 } 220 221 ctx.fontSize(fontSize); 222 ctx.fontFace("sans"); 223 224 import std.algorithm : min; 225 import std.range : isRandomAccessRange; 226 static if (isRandomAccessRange!TreeModel) 227 { 228 foreach(i, item; model) 229 { 230 ctx.save; 231 scope(exit) ctx.restore; 232 233 if (drawItem(ctx, cast(int)(fontSize() * 1.3f), item)) 234 { 235 if (tree_path.length < ctx.tree_view_nesting_level+1) 236 tree_path.length = ctx.tree_view_nesting_level+1; 237 tree_path[ctx.tree_view_nesting_level] = i; 238 } 239 } 240 } 241 else static if (is(TreeModel == struct)) 242 { 243 ctx.save; 244 scope(exit) ctx.restore; 245 246 if (drawItem(ctx, cast(int)(fontSize() * 1.3f), model)) 247 { 248 if (tree_path.length < ctx.tree_view_nesting_level+1) 249 tree_path.length = ctx.tree_view_nesting_level+1; 250 tree_path[ctx.tree_view_nesting_level] = 0; 251 } 252 } 253 else 254 { 255 // static assert(0, "Unsupported type of TreeModel: " ~ TreeModel.stringof); 256 drawItem(ctx, cast(int)(fontSize() * 1.3f), model); 257 } 258 } 259 } 260 261 // // Saves this TreeView to the specified Serializer. 262 //override void save(Serializer &s) const; 263 264 // // Loads the state of the specified Serializer to this TreeView. 265 //override bool load(Serializer &s); 266 267 protected: 268 /// The caption text of this TreeView. 269 string mCaption; 270 271 /** 272 * Internal tracking variable to distinguish between mouse click and release. 273 * `nanogui.TreeView.mCallback` is only called upon release. See 274 * `nanogui.TreeView.mouseButtonEvent` for specific conditions. 275 */ 276 bool mPushed; 277 278 /// Whether or not this TreeView is currently checked or unchecked. 279 bool _mChecked; 280 281 bool mChecked() const { return _mChecked; }; 282 auto mChecked(bool v) 283 { 284 if (_mChecked != v) 285 { 286 _mChecked = v; 287 import std.stdio; 288 writeln("mChecked changed: ", v); 289 mSize += Vector2i(0, v ? 100 : -100); 290 screen.needToPerfomLayout = true; 291 } 292 } 293 294 TreeModel model; 295 296 /// The function to execute when `nanogui.TreeView.mChecked` is changed. 297 void delegate(bool) mCallback; 298 299 // sequence of indices to get access to current element of current treeview 300 size_t[] tree_path; 301 // // number of current dimension (nesting level) of current tree path 302 // size_t current_dimension; 303 }