1 module scrolling; 2 3 /+ 4 + Copyright Andrej Mitrovic 2011. 5 + Distributed under the Boost Software License, Version 1.0. 6 + (See accompanying file LICENSE_1_0.txt or copy at 7 + http://www.boost.org/LICENSE_1_0.txt) 8 +/ 9 10 import core.memory; 11 import core.runtime; 12 import core.thread; 13 import core.stdc.config; 14 15 import std.algorithm; 16 import std.array; 17 import std.conv; 18 import std.exception; 19 import std.functional; 20 import std.math; 21 import std.random; 22 import std.range; 23 import std.stdio; 24 import std.string; 25 import std.traits; 26 import std.utf; 27 28 pragma(lib, "gdi32.lib"); 29 30 import windows.windef; 31 import windows.winuser; 32 import windows.wingdi; 33 34 alias std.algorithm.min min; // conflict resolution 35 alias std.algorithm.max max; // conflict resolution 36 37 import cairo.cairo; 38 import cairo.win32; 39 40 alias cairo.cairo.RGB RGB; // conflict resolution 41 42 struct StateContext 43 { 44 Context ctx; 45 46 this(Context ctx) 47 { 48 this.ctx = ctx; 49 ctx.save(); 50 } 51 52 ~this() 53 { 54 ctx.restore(); 55 } 56 57 alias ctx this; 58 } 59 60 class PaintBuffer 61 { 62 this(HDC localHdc, int cxClient, int cyClient) 63 { 64 hdc = localHdc; 65 width = cxClient; 66 height = cyClient; 67 68 hBuffer = CreateCompatibleDC(localHdc); 69 hBitmap = CreateCompatibleBitmap(localHdc, cxClient, cyClient); 70 hOldBitmap = SelectObject(hBuffer, hBitmap); 71 72 surf = new Win32Surface(hBuffer); 73 ctx = Context(surf); 74 initialized = true; 75 } 76 77 ~this() 78 { 79 if (initialized) 80 { 81 clear(); 82 } 83 } 84 85 void clear() 86 { 87 ctx.dispose(); 88 surf.finish(); 89 surf.dispose(); 90 91 SelectObject(hBuffer, hOldBitmap); 92 DeleteObject(hBitmap); 93 DeleteDC(hBuffer); 94 initialized = false; 95 } 96 97 HDC hdc; 98 bool initialized; 99 int width, height; 100 HDC hBuffer; 101 HBITMAP hBitmap; 102 HBITMAP hOldBitmap; 103 Context ctx; 104 Surface surf; 105 } 106 107 abstract class Widget 108 { 109 Widget parent; 110 PAINTSTRUCT ps; 111 PaintBuffer mainPaintBuff; 112 PaintBuffer paintBuffer; 113 HWND hwnd; 114 int width, height; 115 int xOffset, yOffset; 116 bool needsRedraw = true; 117 bool selected; 118 119 this(HWND hwnd, int width, int height) 120 { 121 this.hwnd = hwnd; 122 this.width = width; 123 this.height = height; 124 //~ SetTimer(hwnd, 100, 1, null); 125 } 126 127 @property Size!int size() 128 { 129 return Size!int(width, height); 130 } 131 132 abstract LRESULT process(UINT message, WPARAM wParam, LPARAM lParam) 133 { 134 switch (message) 135 { 136 case WM_ERASEBKGND: 137 { 138 return 1; 139 } 140 141 case WM_PAINT: 142 { 143 OnPaint(hwnd, message, wParam, lParam); 144 return 0; 145 } 146 147 case WM_SIZE: 148 { 149 width = cast(short)LOWORD(lParam); 150 height = cast(short)HIWORD(lParam); 151 152 auto localHdc = GetDC(hwnd); 153 154 if (paintBuffer !is null) 155 { 156 paintBuffer.clear(); 157 } 158 159 paintBuffer = new PaintBuffer(localHdc, width, height); 160 ReleaseDC(hwnd, localHdc); 161 162 needsRedraw = true; 163 InvalidateRect(hwnd, null, true); 164 return 0; 165 } 166 167 case WM_TIMER: 168 { 169 InvalidateRect(hwnd, null, true); 170 return 0; 171 } 172 173 case WM_MOVE: 174 { 175 xOffset = cast(short)LOWORD(lParam); 176 yOffset = cast(short)HIWORD(lParam); 177 return 0; 178 } 179 180 case WM_LBUTTONDOWN: 181 { 182 selected ^= 1; // flip 183 needsRedraw = true; 184 InvalidateRect(hwnd, null, false); 185 return 0; 186 } 187 188 case WM_DESTROY: 189 { 190 paintBuffer.clear(); 191 PostQuitMessage(0); 192 return 0; 193 } 194 195 default: 196 } 197 198 return DefWindowProc(hwnd, message, wParam, lParam); 199 } 200 201 abstract void OnPaint(HWND hwnd, UINT message, WPARAM wParam, LPARAM lParam); 202 abstract void draw(StateContext ctx); 203 } 204 205 class TestWidget2 : Widget 206 { 207 this(HWND hwnd, int width, int height) 208 { 209 super(hwnd, width, height); 210 } 211 212 override LRESULT process(UINT message, WPARAM wParam, LPARAM lParam) 213 { 214 return super.process(message, wParam, lParam); 215 } 216 217 override void OnPaint(HWND hwnd, UINT message, WPARAM wParam, LPARAM lParam) 218 { 219 auto ctx = paintBuffer.ctx; 220 auto hBuffer = paintBuffer.hBuffer; 221 auto hdc = BeginPaint(hwnd, &ps); 222 auto boundRect = ps.rcPaint; 223 224 if (needsRedraw) 225 { 226 draw(StateContext(ctx)); 227 needsRedraw = false; 228 } 229 230 with (boundRect) 231 { 232 BitBlt(hdc, left, top, right - left, bottom - top, paintBuffer.hBuffer, left, top, SRCCOPY); 233 } 234 235 EndPaint(hwnd, &ps); 236 } 237 238 override void draw(StateContext ctx) 239 { 240 ctx.setSourceRGB(1, 1, 1); 241 ctx.paint(); 242 243 ctx.save(); 244 ctx.scale(width, height); 245 ctx.moveTo(0, 0); 246 247 ctx.rectangle(0, 0, 1, 1); 248 ctx.setSourceRGBA(1, 1, 1, 0); 249 ctx.setOperator(Operator.CAIRO_OPERATOR_CLEAR); 250 ctx.fill(); 251 252 ctx.setSourceRGB(0, 0, 0); 253 ctx.setOperator(Operator.CAIRO_OPERATOR_OVER); 254 255 auto linpat = new LinearGradient(0, 0, 1, 1); 256 linpat.addColorStopRGB(0, RGB(0, 0.3, 0.8)); 257 linpat.addColorStopRGB(1, RGB(0, 0.8, 0.3)); 258 259 auto radpat = new RadialGradient(0.5, 0.5, 0.25, 0.5, 0.5, 0.75); 260 radpat.addColorStopRGBA(0, RGBA(0, 0, 0, 1)); 261 radpat.addColorStopRGBA(0.5, RGBA(0, 0, 0, 0)); 262 263 ctx.setSource(linpat); 264 ctx.mask(radpat); 265 ctx.restore(); 266 267 if (selected) 268 { 269 ctx.moveTo(0, 0); 270 ctx.setLineWidth(3); 271 ctx.setSourceRGB(1, 0, 0); 272 ctx.rectangle(0, 0, width, height); 273 ctx.stroke(); 274 } 275 } 276 } 277 278 class TestWidget : Widget 279 { 280 RGB backColor; 281 282 this(HWND hwnd, int width, int height) 283 { 284 super(hwnd, width, height); 285 this.backColor = RGB(1, 0, 0); 286 287 auto localHdc = GetDC(hwnd); 288 auto hWindow = CreateWindow(WidgetClass.toUTF16z, null, 289 WS_CHILDWINDOW | WS_VISIBLE | WS_CLIPCHILDREN, // WS_CLIPCHILDREN is necessary 290 0, 0, 0, 0, 291 hwnd, cast(HANDLE)1, // child ID 292 cast(HINSTANCE)GetWindowLongPtr(hwnd, GWL_HINSTANCE), // hInstance 293 null); 294 295 auto widget = new TestWidget2(hWindow, width / 2, width / 2); 296 WidgetHandles[hWindow] = widget; 297 298 auto size = widget.size; 299 MoveWindow(hWindow, size.width / 2, size.height / 2, size.width, size.height, true); 300 } 301 302 override LRESULT process(UINT message, WPARAM wParam, LPARAM lParam) 303 { 304 return super.process(message, wParam, lParam); 305 } 306 307 override void OnPaint(HWND hwnd, UINT message, WPARAM wParam, LPARAM lParam) 308 { 309 auto ctx = paintBuffer.ctx; 310 auto hBuffer = paintBuffer.hBuffer; 311 auto hdc = BeginPaint(hwnd, &ps); 312 auto boundRect = ps.rcPaint; 313 314 if (needsRedraw) 315 { 316 draw(StateContext(ctx)); 317 needsRedraw = false; 318 } 319 320 with (boundRect) 321 { 322 BitBlt(hdc, left, top, right - left, bottom - top, paintBuffer.hBuffer, left, top, SRCCOPY); 323 } 324 325 EndPaint(hwnd, &ps); 326 } 327 328 override void draw(StateContext ctx) 329 { 330 ctx.save(); 331 ctx.scale(width, height); 332 ctx.moveTo(0, 0); 333 334 ctx.rectangle(0, 0, 1, 1); 335 ctx.setSourceRGBA(1, 1, 1, 0); 336 ctx.setOperator(Operator.CAIRO_OPERATOR_CLEAR); 337 ctx.fill(); 338 339 ctx.setSourceRGB(0, 0, 0); 340 ctx.setOperator(Operator.CAIRO_OPERATOR_OVER); 341 342 auto linpat = new LinearGradient(0, 0, 1, 1); 343 linpat.addColorStopRGB(0, RGB(0, 0.3, 0.8)); 344 linpat.addColorStopRGB(1, RGB(0, 0.8, 0.3)); 345 346 auto radpat = new RadialGradient(0.5, 0.5, 0.25, 0.5, 0.5, 0.75); 347 radpat.addColorStopRGBA(0, RGBA(0, 0, 0, 1)); 348 radpat.addColorStopRGBA(0.5, RGBA(0, 0, 0, 0)); 349 350 ctx.setSource(linpat); 351 ctx.mask(radpat); 352 353 ctx.moveTo(0.1, 0.5); 354 ctx.restore(); 355 356 ctx.setSourceRGB(1, 1, 1); 357 ctx.selectFontFace("Tahoma", FontSlant.CAIRO_FONT_SLANT_NORMAL, FontWeight.CAIRO_FONT_WEIGHT_NORMAL); 358 ctx.setFontSize(20); 359 ctx.showText("weeeeeeeeeeeeeeeeeeeeeeeeeee"); 360 361 if (selected) 362 { 363 ctx.moveTo(0, 0); 364 ctx.setLineWidth(3); 365 ctx.setSourceRGB(1, 0, 0); 366 ctx.rectangle(0, 0, width, height); 367 ctx.stroke(); 368 } 369 } 370 } 371 372 /* A place to hold Widget objects. Since each window has a unique HWND, 373 * we can use this hash type to store references to Widgets and call 374 * their window processing methods. 375 */ 376 Widget[HWND] WidgetHandles; 377 378 /* 379 * All Widget windows have this window procedure registered via RegisterClass(), 380 * we use it to dispatch to the appropriate Widget window processing method. 381 * 382 * A similar technique is used in the DFL and DGUI libraries for all of its 383 * windows and widgets. 384 */ 385 extern (Windows) 386 LRESULT winDispatch(HWND hwnd, UINT message, WPARAM wParam, LPARAM lParam) 387 { 388 auto widget = hwnd in WidgetHandles; 389 390 if (widget !is null) 391 { 392 return widget.process(message, wParam, lParam); 393 } 394 395 return DefWindowProc(hwnd, message, wParam, lParam); 396 } 397 398 extern (Windows) 399 LRESULT mainWinProc(HWND hwnd, UINT message, WPARAM wParam, LPARAM lParam) 400 { 401 static PaintBuffer paintBuffer; 402 static int width, height; 403 static int TimerID = 16; 404 405 // Note: this was copied from another example, most of these are not required here 406 static int cxChar, cxCaps, cyChar, cxClient, cyClient, iMaxWidth; 407 static int iDeltaPerLine, iAccumDelta; // for mouse wheel logic 408 int i, x, y, iVertPos, iHorzPos, iPaintStart, iPaintEnd; 409 SCROLLINFO si; 410 TEXTMETRIC tm; 411 ULONG ulScrollLines; // for mouse wheel logic 412 413 static HMENU widgetID = cast(HMENU)0; // todo: each widget has its own HMENU ID 414 415 void draw(StateContext ctx) 416 { 417 ctx.setSourceRGB(0.2, 0.2, 0.2); 418 ctx.paint(); 419 } 420 421 switch (message) 422 { 423 case WM_CREATE: 424 auto hdc = GetDC(hwnd); 425 426 GetTextMetrics(hdc, &tm); 427 cxChar = tm.tmAveCharWidth; 428 cxCaps = (tm.tmPitchAndFamily & 1 ? 3 : 2) * cxChar / 2; 429 cyChar = tm.tmHeight + tm.tmExternalLeading; 430 431 ReleaseDC(hwnd, hdc); 432 433 // Save the width of the three columns 434 iMaxWidth = 40 * cxChar + 22 * cxCaps; 435 436 auto hDesk = GetDesktopWindow(); 437 RECT rc; 438 GetClientRect(hDesk, &rc); 439 440 auto localHdc = GetDC(hwnd); 441 paintBuffer = new PaintBuffer(localHdc, rc.right, rc.bottom); 442 443 auto hWindow = CreateWindow(WidgetClass.toUTF16z, null, 444 WS_CHILDWINDOW | WS_VISIBLE | WS_CLIPCHILDREN, // WS_CLIPCHILDREN is necessary 445 0, 0, 0, 0, 446 hwnd, widgetID, // child ID 447 cast(HINSTANCE)GetWindowLongPtr(hwnd, GWL_HINSTANCE), // hInstance 448 null); 449 450 auto widget = new TestWidget(hWindow, 400, 400); 451 WidgetHandles[hWindow] = widget; 452 453 auto size = widget.size; 454 MoveWindow(hWindow, size.width / 3, size.width / 3, size.width, size.height, true); 455 456 goto case; 457 458 // Fall through for mouse wheel information 459 case WM_SETTINGCHANGE: 460 { 461 SystemParametersInfo(SPI_GETWHEELSCROLLLINES, 0, &ulScrollLines, 0); 462 463 // ulScrollLines usually equals 3 or 0 (for no scrolling) 464 // WHEEL_DELTA equals 120, so iDeltaPerLine will be 40 465 if (ulScrollLines) 466 iDeltaPerLine = WHEEL_DELTA / ulScrollLines; 467 else 468 iDeltaPerLine = 0; 469 470 return 0; 471 } 472 473 case WM_LBUTTONDOWN: 474 { 475 SetFocus(hwnd); 476 return 0; 477 } 478 479 case WM_SIZE: 480 { 481 width = LOWORD(lParam); 482 height = HIWORD(lParam); 483 484 enum windowLength = 100; 485 486 // Set vertical scroll bar range and page size 487 si.fMask = SIF_RANGE | SIF_PAGE; 488 si.nMin = 0; 489 si.nMax = windowLength; 490 si.nPage = height / cyChar; 491 SetScrollInfo(hwnd, SB_VERT, &si, TRUE); 492 493 // Set horizontal scroll bar range and page size 494 si.fMask = SIF_RANGE | SIF_PAGE; 495 si.nMin = 0; 496 si.nMax = 2 + iMaxWidth / cxChar; 497 si.nPage = width / cxChar; 498 SetScrollInfo(hwnd, SB_HORZ, &si, TRUE); 499 return 0; 500 } 501 502 case WM_VSCROLL: 503 { 504 // Get all the vertical scroll bar information 505 si.fMask = SIF_ALL; 506 GetScrollInfo(hwnd, SB_VERT, &si); 507 508 // Save the position for comparison later on 509 iVertPos = si.nPos; 510 511 switch (LOWORD(wParam)) 512 { 513 case SB_TOP: 514 si.nPos = si.nMin; 515 break; 516 517 case SB_BOTTOM: 518 si.nPos = si.nMax; 519 break; 520 521 case SB_LINEUP: 522 si.nPos -= 1; 523 break; 524 525 case SB_LINEDOWN: 526 si.nPos += 1; 527 break; 528 529 case SB_PAGEUP: 530 si.nPos -= si.nPage; 531 break; 532 533 case SB_PAGEDOWN: 534 si.nPos += si.nPage; 535 break; 536 537 case SB_THUMBTRACK: 538 si.nPos = si.nTrackPos; 539 break; 540 541 default: 542 break; 543 } 544 545 // Set the position and then retrieve it. Due to adjustments 546 // by Windows it may not be the same as the value set. 547 si.fMask = SIF_POS; 548 SetScrollInfo(hwnd, SB_VERT, &si, TRUE); 549 GetScrollInfo(hwnd, SB_VERT, &si); 550 551 // If the position has changed, scroll the window and update it 552 if (si.nPos != iVertPos) 553 { 554 auto rect = RECT(0, 0, width, 400); 555 // second to last argument is area that is scrolled 556 //~ ScrollWindow(hwnd, 0, cyChar * (iVertPos - si.nPos), null, &rect); // last arg is boundary 557 ScrollWindow(hwnd, 0, cyChar * (iVertPos - si.nPos), null, null); 558 UpdateWindow(hwnd); 559 } 560 561 return 0; 562 } 563 564 case WM_HSCROLL: 565 { 566 // Get all the vertical scroll bar information 567 si.fMask = SIF_ALL; 568 569 // Save the position for comparison later on 570 GetScrollInfo(hwnd, SB_HORZ, &si); 571 iHorzPos = si.nPos; 572 573 switch (LOWORD(wParam)) 574 { 575 case SB_LINELEFT: 576 si.nPos -= 1; 577 break; 578 579 case SB_LINERIGHT: 580 si.nPos += 1; 581 break; 582 583 case SB_PAGELEFT: 584 si.nPos -= si.nPage; 585 break; 586 587 case SB_PAGERIGHT: 588 si.nPos += si.nPage; 589 break; 590 591 case SB_THUMBPOSITION: 592 si.nPos = si.nTrackPos; 593 break; 594 595 default: 596 break; 597 } 598 599 // Set the position and then retrieve it. Due to adjustments 600 // by Windows it may not be the same as the value set. 601 si.fMask = SIF_POS; 602 SetScrollInfo(hwnd, SB_HORZ, &si, TRUE); 603 GetScrollInfo(hwnd, SB_HORZ, &si); 604 605 // If the position has changed, scroll the window 606 if (si.nPos != iHorzPos) 607 { 608 ScrollWindow(hwnd, cxChar * (iHorzPos - si.nPos), 0, null, null); 609 } 610 611 return 0; 612 } 613 614 case WM_MOUSEWHEEL: 615 { 616 if (iDeltaPerLine == 0) // no scroll 617 break; 618 619 iAccumDelta += cast(short) HIWORD(wParam); // 120 or -120 620 621 while (iAccumDelta >= iDeltaPerLine) 622 { 623 SendMessage(hwnd, WM_VSCROLL, SB_LINEUP, 0); 624 iAccumDelta -= iDeltaPerLine; 625 } 626 627 while (iAccumDelta <= -iDeltaPerLine) 628 { 629 SendMessage(hwnd, WM_VSCROLL, SB_LINEDOWN, 0); 630 iAccumDelta += iDeltaPerLine; 631 } 632 633 return 0; 634 } 635 636 case WM_PAINT: 637 { 638 auto ctx = paintBuffer.ctx; 639 auto hBuffer = paintBuffer.hBuffer; 640 PAINTSTRUCT ps; 641 auto hdc = BeginPaint(hwnd, &ps); 642 auto boundRect = ps.rcPaint; 643 644 draw(StateContext(paintBuffer.ctx)); 645 646 with (boundRect) 647 { 648 BitBlt(hdc, left, top, right - left, bottom - top, paintBuffer.hBuffer, left, top, SRCCOPY); 649 } 650 651 EndPaint(hwnd, &ps); 652 return 0; 653 } 654 655 case WM_TIMER: 656 { 657 InvalidateRect(hwnd, null, true); 658 return 0; 659 } 660 661 case WM_DESTROY: 662 { 663 paintBuffer.clear(); 664 PostQuitMessage(0); 665 return 0; 666 } 667 668 default: 669 } 670 671 return DefWindowProc(hwnd, message, wParam, lParam); 672 } 673 674 string WidgetClass = "WidgetClass"; 675 676 int myWinMain(HINSTANCE hInstance, HINSTANCE hPrevInstance, LPSTR lpCmdLine, int iCmdShow) 677 { 678 string appName = "layered drawing"; 679 680 HWND hwnd; 681 MSG msg; 682 WNDCLASS wndclass; 683 684 /* One class for the main window */ 685 wndclass.lpfnWndProc = &mainWinProc; 686 wndclass.cbClsExtra = 0; 687 wndclass.cbWndExtra = 0; 688 wndclass.hInstance = hInstance; 689 wndclass.hIcon = LoadIcon(null, IDI_APPLICATION); 690 wndclass.hCursor = LoadCursor(null, IDC_ARROW); 691 wndclass.hbrBackground = null; 692 wndclass.lpszMenuName = null; 693 wndclass.lpszClassName = appName.toUTF16z; 694 695 if (!RegisterClass(&wndclass)) 696 { 697 MessageBox(null, "This program requires Windows NT!", appName.toUTF16z, MB_ICONERROR); 698 return 0; 699 } 700 701 /* Separate window class for Widgets. */ 702 wndclass.hbrBackground = null; 703 wndclass.lpfnWndProc = &winDispatch; 704 wndclass.cbWndExtra = 0; 705 wndclass.hIcon = null; 706 wndclass.lpszClassName = WidgetClass.toUTF16z; 707 708 if (!RegisterClass(&wndclass)) 709 { 710 MessageBox(null, "This program requires Windows NT!", appName.toUTF16z, MB_ICONERROR); 711 return 0; 712 } 713 714 hwnd = CreateWindow(appName.toUTF16z, "layered drawing", 715 WS_OVERLAPPEDWINDOW | WS_CLIPCHILDREN, // WS_CLIPCHILDREN is necessary 716 CW_USEDEFAULT, CW_USEDEFAULT, 717 CW_USEDEFAULT, CW_USEDEFAULT, 718 null, null, hInstance, null); 719 720 ShowWindow(hwnd, iCmdShow); 721 UpdateWindow(hwnd); 722 723 while (GetMessage(&msg, null, 0, 0)) 724 { 725 TranslateMessage(&msg); 726 DispatchMessage(&msg); 727 } 728 729 return msg.wParam; 730 } 731 732 extern (Windows) 733 int WinMain(HINSTANCE hInstance, HINSTANCE hPrevInstance, LPSTR lpCmdLine, int iCmdShow) 734 { 735 int result; 736 737 738 try 739 { 740 Runtime.initialize(); 741 myWinMain(hInstance, hPrevInstance, lpCmdLine, iCmdShow); 742 Runtime.terminate(); 743 } 744 catch (Throwable o) 745 { 746 MessageBox(null, o.toString().toUTF16z, "Error", MB_OK | MB_ICONEXCLAMATION); 747 result = -1; 748 } 749 750 return result; 751 }