1 module rpui.widgets.panel.widget; 2 3 import std.container; 4 import std.algorithm.comparison; 5 import std.stdio; 6 7 import rpdl; 8 9 import rpui.primitives; 10 import rpui.widget; 11 import rpui.scroll; 12 import rpui.input; 13 import rpui.view; 14 import rpui.cursor; 15 import rpui.events; 16 import rpui.widget_events; 17 import rpui.math; 18 import rpui.render.renderer; 19 import rpui.render.components : State; 20 21 import rpui.widgets.panel.renderer; 22 import rpui.widgets.panel.scroll_button; 23 import rpui.widgets.panel.theme_loader; 24 25 /** 26 * Panel widget is the container for other widgets with scrolling, 27 * resizing, allow change placement by drag and drop. 28 */ 29 class Panel : Widget, FocusScrollNavigation { 30 enum Background { 31 transparent, /// Render without color. 32 light, 33 dark, 34 action /// Color for actions like OK, Cancel etc. 35 } 36 37 @field float minSize = 40; /// Minimum size of panel. 38 @field float maxSize = 999; /// Maximum size of panel. 39 @field Background background = Background.light; /// Background color of panel. 40 @field bool userCanResize = true; 41 @field bool userCanHide = false; 42 @field bool userCanDrag = false; 43 44 /// If true, then panel is open and will be rendered all content else only header. 45 @field bool isOpen = true; 46 @field bool darkSplit = false; /// If true, then panel split will be dark. 47 @field bool showSplit = true; /// If true, render panel split else no. 48 49 @field bool showVerticalScrollButton = true; 50 @field bool showHorizontalScrollButton = true; 51 52 @field utf32string caption = ""; 53 @field RegionAlign splitAlign = RegionAlign.client; 54 55 package @property inout(RegionAlign) splitRegionAlign() inout { 56 return regionAlign == RegionAlign.client || regionAlign == RegionAlign.none 57 ? oppositeRegionAlign(splitAlign) 58 : regionAlign; 59 } 60 61 struct Measure { 62 float headerHeight; 63 float scrollButtonMinSize; 64 float horizontalScrollRegionWidth; 65 float verticalScrollRegionWidth; 66 float splitThickness; 67 } 68 69 struct Split { 70 bool isClick; 71 bool isEnter; 72 float cursorRangeSize = 8; 73 Rect cursorRangeRect; 74 float thickness; 75 } 76 77 struct Header { 78 float height = 0; 79 bool isEnter = false; 80 } 81 82 package Measure measure; 83 package PanelThemeLoader themeLoader; 84 85 private vec2 lastSize = 0; 86 87 package Split split; 88 package Header header; 89 package ScrollButton horizontalScrollButton = ScrollButton(Orientation.horizontal); 90 package ScrollButton verticalScrollButton = ScrollButton(Orientation.vertical); 91 92 package @property inout(State) headerState() inout { 93 if (isEnter) { 94 return State.enter; 95 } else { 96 return State.leave; 97 } 98 } 99 100 this(in string style = "Panel") { 101 super(style); 102 skipFocus = true; 103 renderer = new PanelRenderer(); 104 } 105 106 override void renderChildren() { 107 if (!drawChildren) 108 return; 109 110 const scissor = getScissor(); 111 view.pushScissor(scissor); 112 113 foreach (Widget child; children) { 114 if (!child.isVisible) 115 continue; 116 117 if (!pointInRect(view.mousePos, scissor)) { 118 child.isEnter = false; 119 child.isClick = false; 120 } 121 122 child.onRender(); 123 } 124 125 view.popScissor(); 126 } 127 128 protected override void onCreate() { 129 super.onCreate(); 130 131 measure = themeLoader.createMeasureFromRpdl(view.theme.tree.data, style); 132 133 header.height = measure.headerHeight; 134 split.thickness = measure.splitThickness; 135 136 horizontalScrollButton.attach(this); 137 verticalScrollButton.attach(this); 138 } 139 140 override void onRender() { 141 renderer.onRender(); 142 } 143 144 override void onProgress(in ProgressEvent event) { 145 super.onProgress(event); 146 147 split.isEnter = false; 148 149 handleResize(); 150 headerOnProgress(); 151 152 // Update render elements position and sizes 153 locator.updateRegionAlign(); 154 locator.updateAbsolutePosition(); 155 156 updateInnerOffset(); 157 updateSize(); 158 159 with (horizontalScrollButton) 160 contentOffset.x = visible ? scrollController.contentOffset : 0; 161 162 with (verticalScrollButton) 163 contentOffset.y = visible ? scrollController.contentOffset : 0; 164 165 if (!isFreezingSource() && !isFrozen()) { 166 horizontalScrollButton.onProgress(); 167 verticalScrollButton.onProgress(); 168 } else { 169 horizontalScrollButton.isEnter = false; 170 verticalScrollButton.isEnter = false; 171 } 172 } 173 174 void headerOnProgress() { 175 if (!userCanHide) 176 return; 177 178 const vec2 headerSize = vec2(size.x, header.height); 179 const Rect rect = Rect(absolutePosition, headerSize); 180 header.isEnter = pointInRect(view.mousePos, rect); 181 } 182 183 override void updateSize() { 184 if (isOpen) { 185 updatePanelSize(); 186 187 horizontalScrollButton.updateSize(); 188 verticalScrollButton.updateSize(); 189 } 190 191 with (horizontalScrollButton) 192 contentOffset.x = visible ? scrollController.contentOffset : 0; 193 194 with (verticalScrollButton) 195 contentOffset.y = visible ? scrollController.contentOffset : 0; 196 } 197 198 private void updatePanelSize() { 199 if (heightType == SizeType.wrapContent) { 200 size.y = innerBoundarySize.y; 201 } 202 203 if (widthType == SizeType.wrapContent) { 204 size.x = innerBoundarySize.x; 205 } 206 } 207 208 /// Add extra inner offset depends of which elements are visible. 209 protected void updateInnerOffset() { 210 extraInnerOffset.left = 0; 211 212 if (verticalScrollButton.visible) { 213 extraInnerOffset.right = verticalScrollButton.width; 214 } else { 215 extraInnerOffset.right = 0; 216 } 217 218 if (horizontalScrollButton.visible) { 219 extraInnerOffset.bottom = horizontalScrollButton.width; 220 } else { 221 extraInnerOffset.bottom = 0; 222 } 223 224 if (userCanHide) { 225 extraInnerOffset.top = header.height; 226 } else { 227 extraInnerOffset.top = 0; 228 } 229 230 // Split extra inner offset 231 if (userCanResize || showSplit) { 232 const thickness = 1; 233 234 switch (regionAlign) { 235 case RegionAlign.top: 236 extraInnerOffset.bottom += thickness; 237 break; 238 239 case RegionAlign.bottom: 240 extraInnerOffset.top += thickness; 241 break; 242 243 case RegionAlign.right: 244 extraInnerOffset.left += thickness; 245 break; 246 247 case RegionAlign.left: 248 extraInnerOffset.right += thickness; 249 break; 250 251 default: 252 break; 253 } 254 } 255 } 256 257 // Resize panel when split is clicked. 258 void handleResize() { 259 if (!split.isClick) 260 return; 261 262 switch (regionAlign) { 263 case RegionAlign.top: 264 size.y = lastSize.y + view.mousePos.y - view.mouseClickPos.y; 265 break; 266 267 case RegionAlign.bottom: 268 size.y = lastSize.y - view.mousePos.y + view.mouseClickPos.y; 269 break; 270 271 case RegionAlign.left: 272 size.x = lastSize.x + view.mousePos.x - view.mouseClickPos.x; 273 break; 274 275 case RegionAlign.right: 276 size.x = lastSize.x - view.mousePos.x + view.mouseClickPos.x; 277 break; 278 279 default: 280 break; 281 } 282 283 if (regionAlign == RegionAlign.top || regionAlign == RegionAlign.bottom) 284 size.y = clamp(size.y, minSize, maxSize); 285 286 if (regionAlign == RegionAlign.left || regionAlign == RegionAlign.right) 287 size.x = clamp(size.x, minSize, maxSize); 288 289 parent.events.notify(ResizeEvent()); 290 view.rootWidget.updateAll(); 291 } 292 293 private Rect getScissor() { 294 Rect scissor; 295 const thickness = split.thickness; 296 297 scissor.point = absolutePosition + extraInnerOffsetStart; 298 scissor.size = size; 299 300 if (userCanHide) { 301 scissor.size = scissor.size - vec2(0, header.height); 302 } 303 304 if (userCanResize || showSplit) { 305 if (regionAlign == RegionAlign.top || regionAlign == RegionAlign.bottom) { 306 // Horizontal orientation 307 scissor.size = scissor.size - vec2(0, thickness); 308 } 309 else if (regionAlign == RegionAlign.left || regionAlign == RegionAlign.right) { 310 // Vertical orientation 311 scissor.size = scissor.size - vec2(thickness, 0); 312 } 313 } 314 315 return scissor; 316 } 317 318 /// Change system cursor when mouse entering split. 319 override void onCursor() { 320 if (!userCanResize || !isOpen || scrollButtonIsClicked) 321 return; 322 323 if (regionAlign == RegionAlign.top || regionAlign == RegionAlign.bottom) { 324 const Rect rect = Rect( 325 split.cursorRangeRect.left, 326 split.cursorRangeRect.top - split.cursorRangeSize / 2.0f, 327 split.cursorRangeRect.width, 328 split.cursorRangeSize 329 ); 330 331 if (pointInRect(view.mousePos, rect) || split.isClick) { 332 view.cursor = CursorIcon.vDoubleArrow; 333 split.isEnter = true; 334 horizontalScrollButton.isEnter = false; 335 } 336 } 337 else if (regionAlign == RegionAlign.left || regionAlign == RegionAlign.right) { 338 const Rect rect = Rect( 339 split.cursorRangeRect.left - split.cursorRangeSize / 2.0f, 340 split.cursorRangeRect.top, 341 split.cursorRangeSize, 342 split.cursorRangeRect.height 343 ); 344 345 if (pointInRect(view.mousePos, rect) || split.isClick) { 346 view.cursor = CursorIcon.hDoubleArrow; 347 split.isEnter = true; 348 verticalScrollButton.isEnter = false; 349 } 350 } 351 } 352 353 private bool scrollButtonIsClicked() { 354 return verticalScrollButton.isClick || horizontalScrollButton.isClick; 355 } 356 357 override void scrollToWidget(Widget widget) { 358 const vec2 relativePosition = widget.absolutePosition - 359 (absolutePosition + extraInnerOffsetStart); 360 361 with (verticalScrollButton.scrollController) 362 setOffsetInPx(relativePosition.y + contentOffset); 363 364 with (horizontalScrollButton.scrollController) 365 setOffsetInPx(relativePosition.x + contentOffset); 366 } 367 368 override void borderScrollToWidget(Widget widget) { 369 const vec2 relativePosition = widget.absolutePosition - 370 (absolutePosition + extraInnerOffsetStart); 371 372 with (verticalScrollButton.scrollController) { 373 const float innerVisibleSize = visibleSize - extraInnerOffsetSize.y; 374 const float widgetScrollOffset = relativePosition.y + contentOffset; 375 376 if (relativePosition.y < 0) { 377 setOffsetInPx(widgetScrollOffset); 378 } else if (relativePosition.y + widget.size.y > innerVisibleSize) { 379 setOffsetInPx(widgetScrollOffset - innerVisibleSize + widget.size.y); 380 } 381 } 382 383 with (horizontalScrollButton.scrollController) { 384 const float innerVisibleSize = visibleSize - extraInnerOffsetSize.x; 385 const float widgetScrollOffset = relativePosition.x + contentOffset; 386 387 if (relativePosition.x < 0) { 388 setOffsetInPx(widgetScrollOffset); 389 } else if (relativePosition.x + widget.size.x > innerVisibleSize) { 390 setOffsetInPx(widgetScrollOffset - innerVisibleSize + widget.size.x); 391 } 392 } 393 } 394 395 /// Set scroll value in px. 396 void scrollToPx(in float x, in float y) { 397 verticalScrollButton.scrollController.setOffsetInPx(x); 398 horizontalScrollButton.scrollController.setOffsetInPx(y); 399 } 400 401 /// Add value to scroll in px. 402 void scrollByPx(in float dx, in float dy) { 403 verticalScrollButton.scrollController.addOffsetInPx(dx); 404 horizontalScrollButton.scrollController.addOffsetInPx(dy); 405 } 406 407 /// Set scroll value in percent. 408 void scrollToPercent(in float x, in float y) { 409 verticalScrollButton.scrollController.setOffsetInPercent(x); 410 horizontalScrollButton.scrollController.setOffsetInPercent(y); 411 } 412 413 final void open() { 414 if (isOpen) 415 return; 416 417 size = lastSize; 418 isOpen = true; 419 view.rootWidget.updateAll(); 420 } 421 422 final void close() { 423 if (!isOpen) 424 return; 425 426 lastSize = size; 427 size.y = header.height; 428 isOpen = false; 429 view.rootWidget.updateAll(); 430 431 horizontalScrollButton.scrollController.setOffsetInPercent(0); 432 verticalScrollButton.scrollController.setOffsetInPercent(0); 433 } 434 435 /// Toggle visibility of panel. If `isOpen` then method will close panel else open. 436 final void toggle() { 437 if (isOpen) { 438 close(); 439 } else { 440 open(); 441 } 442 } 443 444 // Events ------------------------------------------------------------------------------------------ 445 446 override void onMouseUp(in MouseUpEvent event) { 447 verticalScrollButton.isClick = false; 448 horizontalScrollButton.isClick = false; 449 450 if (split.isClick) { 451 split.isClick = false; 452 unfreezeUI(); 453 } 454 455 super.onMouseUp(event); 456 } 457 458 /// Handle mouse down event - avoid it if UI is forzen. 459 override void onMouseDown(in MouseDownEvent event) { 460 if (isFreezingSource() && view.isNestedFreeze) 461 return; 462 463 if (split.isEnter && isOpen && view.cursor != CursorIcon.inherit) { 464 lastSize = size; 465 split.isClick = true; 466 freezeUI(); 467 } 468 469 if (!isFreezingSource()) { 470 verticalScrollButton.isClick = verticalScrollButton.isEnter; 471 horizontalScrollButton.isClick = horizontalScrollButton.isEnter; 472 473 verticalScrollButton.scrollController.onMouseDown(event); 474 horizontalScrollButton.scrollController.onMouseDown(event); 475 } 476 477 onHeaderMouseDown(); 478 super.onMouseDown(event); 479 } 480 481 private void onHeaderMouseDown() { 482 if (!header.isEnter || !userCanHide) 483 return; 484 485 toggle(); 486 } 487 488 override void onResize() { 489 horizontalScrollButton.scrollController.onResize(); 490 verticalScrollButton.scrollController.onResize(); 491 492 super.onResize(); 493 } 494 495 protected override void onMouseWheelHandle(in int dx, in int dy) { 496 if (isFreezingSource() && view.isNestedFreeze) 497 return; 498 499 Scrollable scrollable = cast(Scrollable) parent; 500 501 int horizontalDelta = dx; 502 int verticalDelta = dy; 503 504 if (!verticalScrollButton.scrollController.addOffsetInPx(-verticalDelta*20)) { 505 if (scrollable && parent.isOver && !parent.isFrozen()) { 506 scrollable.onMouseWheelHandle(0, verticalDelta); 507 } 508 } 509 510 if (!horizontalScrollButton.scrollController.addOffsetInPx(-horizontalDelta*20)) { 511 if (scrollable && parent.isOver && !parent.isFrozen()) { 512 scrollable.onMouseWheelHandle(horizontalDelta, 0); 513 } 514 } 515 } 516 }