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 : MouseButton, Vector2f, Vector2i; 15 import nanogui.experimental.utils : Model, TreePathVisitorImpl; 16 17 /** 18 * Tree view widget. 19 */ 20 class TreeView(Data) : Widget 21 { 22 public: 23 24 enum modelHasCollapsed = is(typeof(Model!Data.collapsed) == bool); 25 26 /** 27 * Adds a TreeView to the specified `parent`. 28 * 29 * Params: 30 * parent = The Widget to add this TreeView to. 31 * caption = The caption text of the TreeView (default `"Untitled"`). 32 * callback = If provided, the callback to execute when the TreeView is 33 * checked or unchecked. Default parameter function does nothing. See 34 * `nanogui.TreeView.mPushed` for the difference between "pushed" 35 * and "checked". 36 */ 37 this(Widget parent, string caption, Data data, void delegate(bool) callback) 38 { 39 super(parent); 40 mCaption = caption; 41 static if (modelHasCollapsed) 42 { 43 mPushed = false; 44 mChecked = false; 45 mCallback = callback; 46 } 47 _data = data; 48 _model = makeModel(_data); 49 import nanogui.experimental.utils : MeasuringVisitor; 50 auto v = MeasuringVisitor([size.x, fontSize]); 51 _model.visitForward(_data, v); 52 } 53 54 /// The caption of this TreeView. 55 final string caption() const { return mCaption; } 56 57 /// Sets the caption of this TreeView. 58 final void caption(string caption) { mCaption = caption; } 59 60 static if (modelHasCollapsed) 61 { 62 /// Whether or not this TreeView is currently checked. 63 final bool checked() const { return _model.collapsed; } 64 65 /// Sets whether or not this TreeView is currently checked. 66 final void checked(bool checked) { _model.collapsed = checked; } 67 68 /// Whether or not this TreeView is currently pushed. See `nanogui.TreeView.mPushed`. 69 final bool pushed() const { return mPushed; } 70 71 /// Sets whether or not this TreeView is currently pushed. See `nanogui.TreeView.mPushed`. 72 final void pushed(bool pushed) { mPushed = pushed; } 73 74 /// Returns the current callback of this TreeView. 75 final void delegate(bool) callback() const { return mCallback; } 76 77 /// Sets the callback to be executed when this TreeView is checked / unchecked. 78 final void callback(void delegate(bool) callback) { mCallback = callback; } 79 } 80 81 /** 82 * The mouse button callback will return `true` when all three conditions are met: 83 * 84 * 1. This TreeView is "enabled" (see `nanogui.Widget.mEnabled`). 85 * 2. `p` is inside this TreeView. 86 * 3. `button` is `MouseButton.Left`. 87 * 88 * Since a mouse button event is issued for both when the mouse is pressed, as well 89 * as released, this function sets `nanogui.TreeView.mPushed` to `true` when 90 * parameter `down == true`. When the second event (`down == false`) is fired, 91 * `nanogui.TreeView.mChecked` is inverted and `nanogui.TreeView.mCallback` 92 * is called. 93 * 94 * That is, the callback provided is only called when the mouse button is released, 95 * **and** the click location remains within the TreeView boundaries. If the user 96 * clicks on the TreeView and releases away from the bounds of the TreeView, 97 * `nanogui.TreeView.mPushed` is simply set back to `false`. 98 */ 99 override bool mouseButtonEvent(Vector2i p, MouseButton button, bool down, int modifiers) 100 { 101 if (!mEnabled) 102 return false; 103 104 static if (modelHasCollapsed) 105 { 106 import nanogui.experimental.utils : isPointInRect; 107 const rect_size = Vector2i(mSize.x, !_model.collapsed ? cast(int) (fontSize() * 1.3f) : mSize.y); 108 109 if (button == MouseButton.Left) 110 { 111 import nanogui.experimental.utils : setPropertyByTreePath, getPropertyByTreePath; 112 if (!down && tree_path.value.length) 113 { 114 const value = getPropertyByTreePath!("collapsed", bool)(_data, _model, tree_path.value[]); 115 if (!value.isNull) 116 { 117 setPropertyByTreePath!"collapsed"(_data, _model, tree_path.value[], !value.get); 118 import nanogui.experimental.utils : MeasuringVisitor; 119 auto mv = MeasuringVisitor([size.x, fontSize]); 120 _model.visitForward(_data, mv); 121 screen.needToPerfomLayout = true; 122 } 123 } 124 if (!isPointInRect(mPos, rect_size, p)) 125 return false; 126 if (down) 127 { 128 mPushed = true; 129 } 130 else if (mPushed) 131 { 132 mChecked = !mChecked; 133 if (mCallback) 134 mCallback(mChecked); 135 mPushed = false; 136 } 137 return true; 138 } 139 } 140 141 return super.mouseButtonEvent(p, button, down, modifiers); 142 } 143 144 /// The preferred size of this TreeView. 145 override Vector2i preferredSize(NanoContext ctx) const 146 { 147 if (mFixedSize != Vector2i()) 148 return mFixedSize; 149 ctx.fontSize(fontSize()); 150 ctx.fontFace("sans"); 151 float[4] bounds; 152 153 return cast(Vector2i) Vector2f( 154 (ctx.textBounds(0, 0, mCaption, bounds[]) + 155 1.8f * fontSize()), 156 _model.size); 157 } 158 159 /// Draws this TreeView. 160 override void draw(ref NanoContext ctx) 161 { 162 import auxil.location : SizeType; 163 // do not call super.draw() because we do custom drawing 164 165 //ctx.fontSize(theme.mButtonFontSize); 166 //ctx.fontFace("sans-bold"); 167 168 ctx.theme = theme; 169 ctx.size = Vector2f(size.x, ctx.fontSize); 170 ctx.position = mPos; 171 172 //ctx.mouse -= mPos; 173 //scope(exit) ctx.mouse += mPos; 174 175 auto renderer = RenderingVisitor(ctx, [cast(SizeType) ctx.size.x, cast(SizeType) mSize.y]); 176 renderer.loc.y.destination = cast(SizeType) (ctx.position.y + size.y); 177 import nanogui.layout : Orientation; 178 renderer.ctx.orientation = Orientation.Vertical; 179 _model.visitForward(_data, renderer); 180 tree_path = renderer.selected_item; 181 } 182 183 // // Saves this TreeView to the specified Serializer. 184 //override void save(Serializer &s) const; 185 186 // // Loads the state of the specified Serializer to this TreeView. 187 //override bool load(Serializer &s); 188 189 protected: 190 191 import nanogui.experimental.utils : makeModel, visit, visitForward, TreePath; 192 193 /// The caption text of this TreeView. 194 string mCaption; 195 196 static if (modelHasCollapsed) 197 { 198 /** 199 * Internal tracking variable to distinguish between mouse click and release. 200 * `nanogui.TreeView.mCallback` is only called upon release. See 201 * `nanogui.TreeView.mouseButtonEvent` for specific conditions. 202 */ 203 bool mPushed; 204 205 bool mChecked() const 206 { 207 static if (is(typeof(_model.collapsed) == bool)) 208 return !_model.collapsed; 209 else 210 return false; 211 } 212 213 auto mChecked(bool v) 214 { 215 static if (is(typeof(_model.collapsed) == bool)) 216 if (_model.collapsed == v) 217 { 218 _model.collapsed = !v; 219 import nanogui.experimental.utils : MeasuringVisitor; 220 auto mv = MeasuringVisitor([size.x, fontSize]); 221 _model.visitForward(_data, mv); 222 screen.needToPerfomLayout = true; 223 } 224 } 225 226 /// The function to execute when `nanogui.TreeView.mChecked` is changed. 227 void delegate(bool) mCallback; 228 } 229 230 Data _data; 231 typeof(makeModel(_data)) _model; 232 233 // sequence of indices to get access to current element of current treeview 234 TreePath tree_path; 235 } 236 237 private struct RenderingVisitor 238 { 239 import nanogui.experimental.utils : drawItem, indent, unindent, TreePath; 240 import auxil.model; 241 import auxil.location : SizeType; 242 243 TreePathVisitorImpl!(typeof(this)) default_visitor; 244 alias default_visitor this; 245 246 NanoContext ctx; 247 TreePath selected_item; 248 float finish; 249 Vector2f origin; 250 251 this(ref NanoContext ctx, SizeType[2] size) 252 { 253 this.ctx = ctx; 254 default_visitor = typeof(default_visitor)(size); 255 } 256 257 bool complete() 258 { 259 return default_visitor.complete || ctx.position.y > finish; 260 } 261 262 void beforeChildren(Order order, Data, Model)(ref const(Data) data, ref Model model) 263 { 264 ctx.indent; 265 } 266 267 void afterChildren(Order order, Data, Model)(ref const(Data) data, ref Model model) 268 { 269 ctx.unindent; 270 } 271 272 void enterTree(Order order, Data, Model)(ref const(Data) data, ref Model model) 273 { 274 origin = ctx.position; 275 } 276 277 void enterNode(Order order, Data, Model)(ref const(Data) data, ref Model model) 278 if (Model.Collapsable) 279 { 280 ctx.position.x = origin.x + loc.x.position; 281 ctx.position.y = origin.y + loc.y.position; 282 ctx.size[Orientation.Horizontal] = loc.x.size; 283 ctx.size[Orientation.Vertical] = loc.y.size; 284 285 if (orientation == Orientation.Vertical) 286 { 287 // background for icon 288 NVGPaint bg = ctx.boxGradient( 289 ctx.position.x + 1.5f, ctx.position.y + 1.5f, 290 ctx.size[ctx.orientation] - 2.0f, ctx.size[ctx.orientation] - 2.0f, 3, 3, 291 true/*pushed*/ ? Color(0, 0, 0, 100) : Color(0, 0, 0, 32), 292 Color(0, 0, 0, 180) 293 ); 294 295 ctx.beginPath; 296 ctx.roundedRect(ctx.position.x + 1.0f, ctx.position.y + 1.0f, 297 ctx.size[ctx.orientation] - 2.0f, ctx.size[ctx.orientation] - 2.0f, 3); 298 ctx.fillPaint(bg); 299 ctx.fill; 300 301 // icon 302 ctx.fontSize(ctx.size.y); 303 ctx.fontFace("icons"); 304 ctx.fillColor(model.enabled ? ctx.theme.mIconColor 305 : ctx.theme.mDisabledTextColor); 306 NVGTextAlign algn; 307 algn.center = true; 308 algn.middle = true; 309 ctx.textAlign(algn); 310 311 import nanogui.entypo : Entypo; 312 int axis2 = (cast(int)ctx.orientation+1)%2; 313 const old = ctx.size[axis2]; 314 ctx.size[axis2] = ctx.size[ctx.orientation]; // icon has width equals to its height 315 dchar symb = model.collapsed ? Entypo.ICON_CHEVRON_RIGHT : 316 Entypo.ICON_CHEVRON_DOWN; 317 if (drawItem(ctx, ctx.size[ctx.orientation], [symb])) 318 selected_item = loc.current_path; 319 ctx.position.y += ctx.size.y; 320 ctx.size[axis2] = old; // restore full width 321 ctx.position[ctx.orientation] -= ctx.size[ctx.orientation]; 322 323 // Caption 324 ctx.position.x += 1.6f * ctx.size.y; 325 scope(exit) ctx.position.x -= 1.6f * ctx.size.y; 326 ctx.fontSize(ctx.size.y); 327 ctx.fontFace("sans"); 328 ctx.fillColor(model.enabled ? ctx.theme.mTextColor : ctx.theme.mDisabledTextColor); 329 if (drawItem(ctx, ctx.size.y, Data.stringof)) 330 selected_item = loc.current_path; 331 } 332 } 333 334 void enterNode(Order order, Data, Model)(ref const(Data) data, ref Model model) 335 if (!Model.Collapsable) 336 { 337 ctx.position.x = origin.x + loc.x.position; 338 ctx.position.y = origin.y + loc.y.position; 339 ctx.size[Orientation.Horizontal] = loc.x.size; 340 ctx.size[Orientation.Vertical] = loc.y.size; 341 342 ctx.fontSize(ctx.size.y); 343 ctx.fontFace("sans"); 344 ctx.fillColor(ctx.theme.mTextColor); 345 ctx.save; 346 ctx.intersectScissor(ctx.position.x, ctx.position.y, ctx.size.x, ctx.size.y); 347 if (drawItem(ctx, cast(int) ctx.size[ctx.orientation], data)) 348 selected_item = loc.current_path; 349 ctx.restore; 350 } 351 352 void leaveNode(Order order, Data, Model)(ref const(Data) data, ref Model model) {} 353 }