1 /// 2 module nanogui.experimental.list; 3 4 import std.algorithm : min, max; 5 import nanogui.widget; 6 import nanogui.common : MouseButton, Vector2f, Vector2i, NanoContext; 7 import nanogui.experimental.utils : DataItem; 8 9 private class IListImplementor 10 { 11 import nanogui.layout : BoxLayout; 12 13 abstract void size(Vector2i v); 14 abstract void position(Vector2i v); 15 abstract void layout(BoxLayout l); 16 abstract Vector2i preferredSize(NanoContext ctx) const; 17 abstract void performLayout(NanoContext ctx); 18 abstract void currentItemIndicesToHeight(ref float start, ref float finish); 19 abstract void draw(NanoContext ctx); 20 } 21 22 private class ListImplementor(T) : IListImplementor 23 { 24 import std.range : isRandomAccessRange; 25 import nanogui.layout : BoxLayout; 26 27 private 28 { 29 DataItem!T[] _data; 30 BoxLayout _layout; 31 Vector2i _size; 32 Vector2i _pos; 33 List _parent; 34 35 static int _last_id; 36 int _id; 37 38 size_t _scroll_position; 39 size_t _start_item; 40 size_t _finish_item; 41 size_t _shift; 42 } 43 44 @disable this(); 45 46 this(R)(List p, R data) if (isRandomAccessRange!R) 47 { 48 import std.exception : enforce; 49 50 enforce(p); 51 52 _id = ++_id; 53 _parent = p; 54 55 import std.array : array; 56 _data = data.array; 57 _scroll_position = _scroll_position.max-1; 58 } 59 60 override void size(Vector2i v) 61 { 62 _size = v; 63 } 64 65 override void position(Vector2i v) 66 { 67 _pos = v; 68 } 69 70 override void layout(BoxLayout l) 71 { 72 _layout = l; 73 } 74 75 /// Draw the widget (and all child widgets) 76 override void draw(NanoContext ctx) 77 { 78 int fontSize = _parent.theme.mButtonFontSize; 79 ctx.fontSize(fontSize); 80 ctx.fontFace("sans-bold"); 81 82 ctx.save; 83 84 int size_y = (_parent.fixedSize.y) ? _parent.fixedSize.y : _parent.size.y; 85 assert(_size.y >= _parent.size.y); 86 const scroll_position = cast(size_t) (_parent.mScroll * (_size.y - _parent.size.y)); 87 if (_scroll_position != scroll_position) 88 { 89 _shift = heightToItemIndex(_data, scroll_position, size_y, _layout.spacing, _start_item, _finish_item, _shift); 90 _scroll_position = scroll_position; 91 } 92 93 ctx.theme = _parent.theme; 94 ctx.current_size = _parent.size.x; 95 ctx.position.x = _pos.x; 96 ctx.position.y = cast(int) _shift + _pos.y; 97 98 ctx.mouse -= _parent.absolutePosition; 99 scope(exit) ctx.mouse += _parent.absolutePosition; 100 101 import std.algorithm : min; 102 foreach(child; _data[_start_item..min(_finish_item, $)]) 103 { 104 ctx.save; 105 scope(exit) ctx.restore; 106 107 import std.conv : text; 108 child.draw(ctx, text(child.size.y), child.size.y); 109 ctx.position.y += cast(int) _layout.spacing; 110 } 111 ctx.restore; 112 } 113 114 /// Convert given range of items indices to to corresponding List height range 115 private auto itemIndexToHeight(size_t start_index, size_t last_index, ref float start, ref float finish) 116 { 117 import nanogui.layout : BoxLayout; 118 double curr = 0; 119 double spacing = _layout.spacing; 120 size_t idx; 121 assert(start_index < last_index); 122 start = 0; 123 finish = 0; 124 125 foreach(ref const e; _data) 126 { 127 if (idx >= start_index) 128 { 129 start = curr; 130 idx++; 131 curr += e.size.y + spacing; 132 break; 133 } 134 idx++; 135 curr += e.size.y + spacing; 136 } 137 138 if (start_index >= _data.length) 139 { 140 finish = start; 141 return; 142 } 143 144 const low_boundary = ++idx; 145 foreach(ref const e; _data[low_boundary..$]) 146 { 147 if (idx >= last_index) 148 { 149 finish = curr; 150 break; 151 } 152 idx++; 153 curr += e.size.y + spacing; 154 } 155 156 if (last_index >= _data.length) 157 finish = curr + spacing; 158 } 159 160 override void currentItemIndicesToHeight(ref float start, ref float finish) 161 { 162 return itemIndexToHeight(_start_item, _finish_item, start, finish); 163 } 164 165 /// Compute the preferred size of the widget 166 override Vector2i preferredSize(NanoContext ctx) const 167 { 168 static Vector2i[int] size_inited; 169 170 if (_id !in size_inited) 171 size_inited[_id] = Vector2i(); 172 else if (size_inited[_id] != Vector2i()) 173 return size_inited[_id]; 174 175 import nanogui.layout : BoxLayout, Orientation, axisIndex, nextAxisIndex; 176 177 Vector2i size; 178 int yOffset = 0; 179 180 uint visible_widget_count; 181 int axis1 = _layout.orientation.axisIndex; 182 int axis2 = _layout.orientation.nextAxisIndex; 183 foreach(ref dataitem; _data) 184 { 185 if (!dataitem.visible) 186 continue; 187 visible_widget_count++; 188 // accumulate the primary axis size 189 size[axis1] += dataitem.size[axis1]; 190 // the secondary axis size is equal to the max size of dataitems 191 size[axis2] = max(size[axis2], dataitem.size[axis2]); 192 } 193 if (visible_widget_count > 1) 194 size[axis1] += (visible_widget_count - 1) * _layout.spacing; 195 size_inited[_id] = size; 196 return size + Vector2i(0, yOffset); 197 } 198 199 /// Invoke the associated layout generator to properly place child widgets, if any 200 override void performLayout(NanoContext ctx) 201 { 202 _scroll_position++; // little hack to force updating item indices 203 foreach(ref dataitem; _data) 204 { 205 if (!dataitem.visible) 206 continue; 207 208 dataitem.performLayout(ctx); 209 } 210 } 211 212 /// Handle a mouse button event (default implementation: propagate to children) 213 bool mouseButtonEvent(Vector2i p, MouseButton button, bool down, int modifiers) 214 { 215 // foreach_reverse(ch; mChildren) 216 // { 217 // Widget child = ch; 218 // if (child.visible && child.contains(p - mPos) && 219 // child.mouseButtonEvent(p - mPos, button, down, modifiers)) 220 // return true; 221 // } 222 // if (button == MouseButton.Left && down && !mFocused) 223 // requestFocus(); 224 return false; 225 } 226 } 227 228 class List : Widget 229 { 230 import std.range : isRandomAccessRange, ElementType; 231 public: 232 233 this(R)(Widget parent, R range) if (isRandomAccessRange!R) 234 { 235 super(parent); 236 mChildPreferredHeight = 0; 237 mScroll = 0.0f; 238 mUpdateLayout = false; 239 240 alias T = ElementType!R; 241 DataItem!T[] data; 242 data.reserve(range.length); 243 foreach(e; range) 244 { 245 import std.random : uniform; 246 data ~= DataItem!T(e, Vector2i(80, 30 + uniform(0, 30))); 247 } 248 249 list_implementor = new ListImplementor!string(this, data); 250 list_implementor.size = Vector2i(width, height); 251 252 import nanogui.layout : BoxLayout, Orientation; 253 auto layout = new BoxLayout(Orientation.Vertical); 254 layout.margin = 40; 255 layout.setSpacing = 20; 256 list_implementor.layout = layout; 257 } 258 259 /// Return the current scroll amount as a value between 0 and 1. 0 means scrolled to the top and 1 to the bottom. 260 float scroll() const { return mScroll; } 261 /// Set the scroll amount to a value between 0 and 1. 0 means scrolled to the top and 1 to the bottom. 262 void setScroll(float scroll) { mScroll = scroll; } 263 264 override void performLayout(NanoContext ctx) 265 { 266 super.performLayout(ctx); 267 268 if (list_implementor is null) 269 return; 270 271 const list_implementor_preferred_size = list_implementor.preferredSize(ctx); 272 mSize.y = parent.size.y - 2*parent.layout.margin; 273 if (mSize.y < 0) 274 mSize.y = 0; 275 276 mChildPreferredHeight = list_implementor.preferredSize(ctx).y; 277 278 if (mChildPreferredHeight > mSize.y) 279 { 280 auto y = cast(int) (-mScroll*(mChildPreferredHeight - mSize.y)); 281 list_implementor.position = Vector2i(0, y); 282 list_implementor.size = Vector2i(mSize.x-12, mChildPreferredHeight); 283 } 284 else 285 { 286 list_implementor.position = Vector2i(0, 0); 287 list_implementor.size = mSize; 288 mScroll = 0; 289 } 290 list_implementor.performLayout(ctx); 291 } 292 293 override Vector2i preferredSize(NanoContext ctx) const 294 { 295 // always return 0 because the size is defined by the parent container 296 return Vector2i(0, 0); 297 } 298 299 override bool mouseDragEvent(Vector2i p, Vector2i rel, MouseButton button, int modifiers) 300 { 301 if (list_implementor !is null && mChildPreferredHeight > mSize.y) 302 { 303 float scrollh = height * min(1.0f, height / cast(float)mChildPreferredHeight); 304 305 mScroll = max(cast(float) 0.0f, min(cast(float) 1.0f, 306 mScroll + rel.y / cast(float)(mSize.y - 8 - scrollh))); 307 mUpdateLayout = true; 308 return true; 309 } 310 else 311 { 312 return super.mouseDragEvent(p, rel, button, modifiers); 313 } 314 } 315 316 override bool scrollEvent(Vector2i p, Vector2f rel) 317 { 318 if (list_implementor !is null && mChildPreferredHeight > mSize.y) 319 { 320 const scrollAmount = rel.y * 10; 321 mScroll = max(0.0f, min(1.0f, mScroll - scrollAmount/cast(typeof(mScroll))mChildPreferredHeight)); 322 mUpdateLayout = true; 323 return true; 324 } 325 else 326 { 327 return super.scrollEvent(p, rel); 328 } 329 } 330 331 /// Handle a mouse button event (default implementation: propagate to children) 332 override bool mouseButtonEvent(Vector2i p, MouseButton button, bool down, int modifiers) 333 { 334 const r = super.mouseButtonEvent(p, button, down, modifiers); 335 if (p.x < mPos.x + mSize.x - 12) 336 return r; 337 338 if (!down) 339 return false; 340 341 const l = mScroll * height; 342 if (list_implementor !is null && mChildPreferredHeight > mSize.y) 343 { 344 float s, f; 345 list_implementor.currentItemIndicesToHeight(s, f); 346 const scrollAmount = l > p.y ? (f - s) : -(f - s); 347 348 mScroll = max(0.0f, min(1.0f, mScroll - scrollAmount/2/cast(float)mChildPreferredHeight)); 349 mUpdateLayout = true; 350 return true; 351 } 352 return false; 353 } 354 355 override void draw(ref NanoContext ctx) 356 { 357 if (list_implementor is null) 358 return; 359 auto y = cast(int) (-mScroll*(mChildPreferredHeight - mSize.y)); 360 list_implementor.position = Vector2i(0, y); 361 mChildPreferredHeight = list_implementor.preferredSize(ctx).y; 362 float scrollh = max(16, height * 363 min(1.0f, height / cast(float) mChildPreferredHeight)); 364 365 if (mUpdateLayout) 366 { 367 list_implementor.performLayout(ctx); 368 mUpdateLayout = false; 369 } 370 371 ctx.save; 372 ctx.translate(mPos.x, mPos.y); 373 ctx.intersectScissor(0, 0, mSize.x, mSize.y); 374 list_implementor.draw(ctx); 375 ctx.restore; 376 377 if (mChildPreferredHeight <= mSize.y) 378 return; 379 380 NVGPaint paint = ctx.boxGradient( 381 mPos.x + mSize.x - 12 + 1, mPos.y + 4 + 1, 8, 382 mSize.y - 8, 3, 4, Color(0, 0, 0, 32), Color(0, 0, 0, 92)); 383 ctx.beginPath; 384 ctx.roundedRect(mPos.x + mSize.x - 12, mPos.y + 4, 8, 385 mSize.y - 8, 3); 386 ctx.fillPaint(paint); 387 ctx.fill; 388 389 paint = ctx.boxGradient( 390 mPos.x + mSize.x - 12 - 1, 391 mPos.y + 4 + (mSize.y - 8 - scrollh) * mScroll - 1, 8, scrollh, 392 3, 4, Color(220, 220, 220, 100), Color(128, 128, 128, 100)); 393 394 ctx.beginPath; 395 ctx.roundedRect( 396 mPos.x + mSize.x - 12 + 1, 397 mPos.y + 4 + 1 + (mSize.y - 8 - scrollh) * mScroll, 8 - 2, 398 scrollh - 2, 2); 399 ctx.fillPaint(paint); 400 ctx.fill; 401 } 402 // override void save(Serializer &s) const; 403 // override bool load(Serializer &s); 404 protected: 405 int mChildPreferredHeight; 406 float mScroll; 407 bool mUpdateLayout; 408 IListImplementor list_implementor; 409 } 410 411 /// Convert given range of List height to corresponding items indices 412 private auto heightToItemIndex(R)(R data, double start, double delta, double spacing, ref size_t start_index, ref size_t last_index, double e0) 413 { 414 const N = data.length; 415 size_t idx = start_index; 416 assert(delta >= 0); 417 418 if (e0 > start) 419 { 420 assert(0 <= idx && idx < N); 421 for(; idx > 0; idx--) 422 { 423 if (e0 - data[idx-1].size.y - spacing <= start && 424 e0 > start) 425 { 426 start_index = idx-1; 427 e0 -= data[idx-1].size.y + spacing; 428 break; 429 } 430 else 431 { 432 e0 -= data[idx-1].size.y + spacing; 433 } 434 } 435 } 436 else 437 { 438 idx = start_index; 439 for(; idx < N; idx++) 440 { 441 if (e0 <= start && e0 + data[idx].size.y + spacing > start) 442 { 443 start_index = idx; 444 break; 445 } 446 else 447 { 448 e0 += data[idx].size.y + spacing; 449 } 450 } 451 assert(0 <= idx); 452 assert(idx <= N); 453 assert( 454 (e0 <= start && idx == data.length) || 455 (e0 <= start && (e0 + data[idx].size.y + spacing) > start) || 456 (idx == N /*&& e0 == E*/) 457 ); 458 } 459 460 if (idx == N) 461 { 462 // assert(e0 == E); 463 start_index = N - 1; 464 last_index = N; 465 return cast(size_t) e0; // start (and finish too) is beyond the last index 466 } 467 468 auto e1 = e0; 469 last_index = 0; 470 471 for(; idx < N; idx++) 472 { 473 if (e1 > start + delta) 474 { 475 last_index = idx + 1; 476 break; 477 } 478 else 479 e1 += data[idx].size.y + spacing; 480 } 481 482 if (idx == data.length) 483 last_index = idx; // start is before and finish is beyond the last index 484 485 return cast(size_t) e0; 486 }