1 module slider; 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 /+ 11 + This is an example of a custom Slider widget implemented with 12 + CairoD with the help of the Win32 API. 13 + You can change the orientation of the slider, its colors, 14 + its maximum value and the extra padding that allows selection 15 + of the slider within extra bounds. 16 + 17 + Currently it doesn't instantiate multiple sliders. 18 + 19 + It relies heavily on Win32 features, such as widgets being 20 + implemented as windows (windows, buttons, menus, and even the 21 + desktop window are all windows) which have X and Y points relative 22 + to their own window, the ability to capture the mouse position 23 + even when it goes outside a window area, and some other features. 24 +/ 25 26 import core.memory; 27 import core.runtime; 28 import core.thread; 29 import core.stdc.config; 30 31 import std.algorithm; 32 import std.array; 33 import std.conv; 34 import std.exception; 35 import std.functional; 36 import std.math; 37 import std.random; 38 import std.range; 39 import std.stdio; 40 import std.string; 41 import std.traits; 42 import std.utf; 43 44 pragma(lib, "gdi32.lib"); 45 46 import windows.windef; 47 import windows.winuser; 48 import windows.wingdi; 49 50 alias std.algorithm.min min; // conflict resolution 51 alias std.algorithm.max max; // conflict resolution 52 53 import cairo.c.cairo; 54 import cairo.cairo; 55 import cairo.win32; 56 57 alias cairo.cairo.RGB RGB; // conflict resolution 58 59 /* 60 * These should be tracked in one place for the entire app, 61 * otherwise you might end up having multiple widgets with 62 * their own control/shift key states. 63 */ 64 __gshared bool shiftState; 65 __gshared bool controlState; 66 __gshared bool mouseTrack; 67 68 /* Used in painting the slider background/foreground/thumb */ 69 struct SliderColors 70 { 71 RGBA thumbActive; 72 RGBA thumbHover; 73 RGBA thumbIdle; 74 75 RGBA fill; 76 RGBA back; 77 RGBA window; 78 } 79 80 /* Two types of sliders */ 81 enum Axis 82 { 83 vertical, 84 horizontal 85 } 86 87 class SliderWindow 88 { 89 int cxClient, cyClient; /* width, height */ 90 HWND hwnd; 91 RGBA thumbColor; // current active thumb color 92 Axis axis; // slider orientation 93 SliderColors colors; // initialized colors 94 int thumbPos; 95 int size; 96 int thumbSize; 97 int offset; // since round caps add additional pixels, we offset the drawing 98 int step; // used when control key is held 99 bool isActive; 100 101 this(HWND hwnd, Axis axis, int size, int thumbSize, int padding, SliderColors colors) 102 { 103 this.hwnd = hwnd; 104 this.colors = colors; 105 this.axis = axis; 106 this.size = size; 107 this.thumbSize = thumbSize; 108 offset = padding / 2; 109 110 thumbPos = (axis == Axis.horizontal) ? 0 : size - thumbSize; 111 step = (axis == Axis.horizontal) ? 2 : -2; 112 } 113 114 /* Get the mouse offset based on slider orientation */ 115 short getTrackPos(LPARAM lParam) 116 { 117 final switch (axis) 118 { 119 case Axis.vertical: 120 { 121 return cast(short)HIWORD(lParam); 122 } 123 124 case Axis.horizontal: 125 { 126 return cast(short)LOWORD(lParam); 127 } 128 } 129 } 130 131 /* Flips x and y based on slider orientation */ 132 void axisFlip(ref int x, ref int y) 133 { 134 final switch (axis) 135 { 136 case Axis.vertical: 137 { 138 break; 139 } 140 141 case Axis.horizontal: 142 { 143 std.algorithm.swap(x, y); 144 break; 145 } 146 } 147 } 148 149 /* Update the thumb position based on mouse position */ 150 void mouseTrackPos(LPARAM lParam) 151 { 152 auto trackPos = getTrackPos(lParam); 153 154 /* steps: 155 * 1. compensate for offseting the slider drawing 156 * (we do not draw from Point(0, 0) because round caps add more pixels). 157 * 2. position the thumb so its center is at the mouse cursor position. 158 * 3. limit the final value between the minimum and maximum position. 159 */ 160 thumbPos = (max(0, min(trackPos - offset - (thumbSize / 2), size))); 161 } 162 163 /* Get the neutral value (calculated based on axis orientation.) */ 164 int getValue() 165 { 166 final switch (axis) 167 { 168 // vertical sliders have a more natural minimum position at the bottom, 169 // and since the Y axis increases towards the bottom we have to invert 170 // this value. 171 case Axis.vertical: 172 { 173 return (retro(iota(0, size + 1)))[thumbPos]; 174 } 175 176 case Axis.horizontal: 177 { 178 return thumbPos; 179 } 180 } 181 } 182 183 /* Process window messages for this slider */ 184 LRESULT process(UINT message, WPARAM wParam, LPARAM lParam) 185 { 186 HDC hdc; 187 PAINTSTRUCT ps; 188 RECT rc; 189 HDC _buffer; 190 HBITMAP hBitmap; 191 HBITMAP hOldBitmap; 192 switch (message) 193 { 194 case WM_SIZE: 195 { 196 cxClient = LOWORD(lParam); 197 cyClient = HIWORD(lParam); 198 InvalidateRect(hwnd, null, FALSE); 199 return 0; 200 } 201 202 /* We're selected, capture the mouse and track its position, 203 * focus our window (this unfocuses all other windows). 204 */ 205 case WM_LBUTTONDOWN: 206 { 207 mouseTrackPos(lParam); 208 SetCapture(hwnd); 209 mouseTrack = true; 210 SetFocus(hwnd); 211 212 InvalidateRect(hwnd, null, FALSE); 213 return 0; 214 } 215 216 /* 217 * End any mouse tracking. 218 */ 219 case WM_LBUTTONUP: 220 { 221 if (mouseTrack) 222 { 223 ReleaseCapture(); 224 mouseTrack = false; 225 } 226 227 InvalidateRect(hwnd, null, FALSE); 228 return 0; 229 } 230 231 /* 232 * We're focused, change slider settings. 233 */ 234 case WM_SETFOCUS: 235 { 236 isActive = true; 237 thumbColor = colors.thumbActive; 238 InvalidateRect(hwnd, null, FALSE); 239 return 0; 240 } 241 242 /* 243 * We've lost focus, change slider settings. 244 */ 245 case WM_KILLFOCUS: 246 { 247 isActive = false; 248 thumbColor = colors.thumbIdle; 249 InvalidateRect(hwnd, null, FALSE); 250 return 0; 251 } 252 253 /* 254 * If we're tracking the mouse update the 255 * slider thumb position. 256 */ 257 case WM_MOUSEMOVE: 258 { 259 if (mouseTrack) // move thumb 260 { 261 //~ writeln(getValue()); 262 mouseTrackPos(lParam); 263 InvalidateRect(hwnd, null, FALSE); 264 } 265 266 return 0; 267 } 268 269 /* 270 * Mouse wheel can control the slider position too. 271 */ 272 case WM_MOUSEWHEEL: 273 { 274 if (isActive) 275 { 276 OnMouseWheel(cast(short)HIWORD(wParam)); 277 InvalidateRect(hwnd, null, FALSE); 278 } 279 280 return 0; 281 } 282 283 /* 284 * Various keys such as Up/Down/Left/Right/PageUp/ 285 * PageDown/Tab/ and the Shift state can control 286 * the slider thumb position. 287 */ 288 case WM_KEYDOWN: 289 case WM_KEYUP: 290 case WM_CHAR: 291 case WM_DEADCHAR: 292 case WM_SYSKEYDOWN: 293 case WM_SYSKEYUP: 294 case WM_SYSCHAR: 295 case WM_SYSDEADCHAR: 296 { 297 // message: key state, wParam: key ID 298 keyUpdate(message, wParam); 299 InvalidateRect(hwnd, null, FALSE); 300 return 0; 301 } 302 303 /* 304 * The paint routine recreates the Cairo context and double-buffering 305 * mechanism on each WM_PAINT message. You could set up context recreation 306 * only when it's necessary (e.g. on a WM_SIZE message), however this quickly 307 * gets complicated due to Cairo's stateful API. 308 */ 309 case WM_PAINT: 310 { 311 hdc = BeginPaint(hwnd, &ps); 312 GetClientRect(hwnd, &rc); 313 314 auto left = rc.left; 315 auto top = rc.top; 316 auto right = rc.right; 317 auto bottom = rc.bottom; 318 319 auto width = right - left; 320 auto height = bottom - top; 321 auto x = left; 322 auto y = top; 323 324 /* Double buffering */ 325 _buffer = CreateCompatibleDC(hdc); 326 hBitmap = CreateCompatibleBitmap(hdc, width, height); 327 hOldBitmap = SelectObject(_buffer, hBitmap); 328 329 auto surf = new Win32Surface(_buffer); 330 auto ctx = Context(surf); 331 332 drawSlider(ctx); 333 334 // Blit the texture to the screen 335 BitBlt(hdc, 0, 0, width, height, _buffer, x, y, SRCCOPY); 336 337 surf.finish(); 338 surf.dispose(); 339 ctx.dispose(); 340 341 SelectObject(_buffer, hOldBitmap); 342 DeleteObject(hBitmap); 343 DeleteDC(_buffer); 344 345 EndPaint(hwnd, &ps); 346 347 return 0; 348 } 349 350 default: 351 } 352 353 return DefWindowProc(hwnd, message, wParam, lParam); 354 } 355 356 void drawSlider(Context ctx) 357 { 358 /* window backround */ 359 ctx.setSourceRGBA(colors.window); 360 ctx.paint(); 361 362 ctx.translate(offset, offset); 363 364 ctx.setLineWidth(10); 365 ctx.setLineCap(LineCap.CAIRO_LINE_CAP_ROUND); 366 367 /* slider backround */ 368 auto begX = 0; 369 auto begY = 0; 370 auto endX = 0; 371 auto endY = size + thumbSize; 372 373 axisFlip(begX, begY); 374 axisFlip(endX, endY); 375 376 ctx.setSourceRGBA(colors.back); 377 ctx.moveTo(begX, begY); 378 ctx.lineTo(endX, endY); 379 ctx.stroke(); 380 381 /* slider value fill */ 382 begX = 0; 383 // vertical sliders have a minimum position at the bottom. 384 begY = (axis == Axis.horizontal) ? 0 : size + thumbSize; 385 endX = 0; 386 endY = thumbPos + thumbSize; 387 388 axisFlip(begX, begY); 389 axisFlip(endX, endY); 390 391 ctx.setSourceRGBA(colors.fill); 392 ctx.moveTo(begX, begY); 393 ctx.lineTo(endX, endY); 394 ctx.stroke(); 395 396 /* slider thumb */ 397 begX = 0; 398 begY = thumbPos; 399 endX = 0; 400 endY = thumbPos + thumbSize; 401 402 axisFlip(begX, begY); 403 axisFlip(endX, endY); 404 405 ctx.setSourceRGBA(thumbColor); 406 ctx.moveTo(begX, begY); 407 ctx.lineTo(endX, endY); 408 ctx.stroke(); 409 } 410 411 /* 412 * Process various keys. 413 * This function is continuously called when a key is held, 414 * but we only update the slider position when a key is pressed 415 * down (WM_KEYDOWN), and not when it's released. 416 */ 417 void keyUpdate(UINT keyState, WPARAM wParam) 418 { 419 switch (wParam) 420 { 421 case VK_LEFT: 422 { 423 if (keyState == WM_KEYDOWN) 424 { 425 if (controlState) 426 thumbPos -= step * 2; 427 else 428 thumbPos -= step; 429 } 430 break; 431 } 432 433 case VK_RIGHT: 434 { 435 if (keyState == WM_KEYDOWN) 436 { 437 if (controlState) 438 thumbPos += step * 2; 439 else 440 thumbPos += step; 441 } 442 break; 443 } 444 445 case VK_SHIFT: 446 { 447 shiftState = (keyState == WM_KEYDOWN); 448 break; 449 } 450 451 case VK_CONTROL: 452 { 453 controlState = (keyState == WM_KEYDOWN); 454 break; 455 } 456 457 case VK_UP: 458 { 459 if (keyState == WM_KEYDOWN) 460 { 461 if (controlState) 462 thumbPos += step * 2; 463 else 464 thumbPos += step; 465 } 466 break; 467 } 468 469 case VK_DOWN: 470 { 471 if (keyState == WM_KEYDOWN) 472 { 473 if (controlState) 474 thumbPos -= step * 2; 475 else 476 thumbPos -= step; 477 } 478 break; 479 } 480 481 case VK_HOME: 482 { 483 thumbPos = 0; 484 break; 485 } 486 487 case VK_END: 488 { 489 thumbPos = size; 490 break; 491 } 492 493 case VK_PRIOR: // page up 494 { 495 thumbPos += step * 2; 496 break; 497 } 498 499 case VK_NEXT: // page down 500 { 501 thumbPos -= step * 2; 502 break; 503 } 504 505 /* 506 * this can be used to switch between different slider windows. 507 * However this should ideally be handled in a main window, not here. 508 * We could pass this message back to the main window. 509 * Currently unimplemented. 510 */ 511 case VK_TAB: 512 { 513 //~ if (shiftState) // shift+tab means go the other way 514 //~ SetFocus(slidersRange.back); 515 //~ else 516 //~ SetFocus(slidersRange.front); 517 //~ break; 518 } 519 520 default: 521 } 522 523 // normalize the thumb position 524 thumbPos = (max(0, min(thumbPos, size))); 525 } 526 527 void OnMouseWheel(sizediff_t nDelta) 528 { 529 if (-nDelta/120 > 0) 530 thumbPos -= step; 531 else 532 thumbPos += step; 533 534 thumbPos = (max(0, min(thumbPos, size))); 535 } 536 } 537 538 /* 539 * A place to hold Slider objects. Since each window has a unique HWND, 540 * we can use this hash type to store references to objects and call 541 * their window processing methods. 542 */ 543 SliderWindow[HWND] SliderHandles; 544 545 /* 546 * All Slider windows will have this same window procedure registered via 547 * RegisterClass(), we use it to dispatch to the appropriate class window 548 * processing method. We could also place this inside the Slider class as 549 * a static method, however this kind of dispatch function is actually 550 * useful for dispatching to any number and types of windows. 551 * 552 * A similar technique is used in the DFL and DGUI libraries for all of its 553 * windows and widgets. 554 */ 555 extern (Windows) 556 LRESULT winDispatch(HWND hwnd, UINT message, WPARAM wParam, LPARAM lParam) 557 { 558 auto slider = hwnd in SliderHandles; 559 560 if (slider !is null) 561 { 562 return slider.process(message, wParam, lParam); 563 } 564 565 return DefWindowProc(hwnd, message, wParam, lParam); 566 } 567 568 extern (Windows) 569 LRESULT mainWinProc(HWND hwnd, UINT message, WPARAM wParam, LPARAM lParam) 570 { 571 static int size = 100; 572 static int padding = 20; 573 static int thumbSize = 20; 574 static HWND hSlider; 575 static axis = Axis.vertical; /* vertical or horizontal orientation */ 576 577 switch (message) 578 { 579 case WM_CREATE: 580 { 581 hSlider = CreateWindow(sliderClass.toUTF16z, null, 582 WS_CHILDWINDOW | WS_VISIBLE, 583 0, 0, 0, 0, 584 hwnd, 585 cast(HMENU)(0), // child ID 586 cast(HINSTANCE)GetWindowLongPtr(hwnd, GWL_HINSTANCE), // hInstance 587 null); 588 589 SliderColors colors; 590 with (colors) 591 { 592 thumbActive = RGBA(1, 1, 1, 1); 593 thumbHover = RGBA(1, 1, 0, 1); 594 thumbIdle = RGBA(1, 0, 0, 1); 595 596 fill = RGBA(1, 0, 0, 1); 597 back = RGBA(1, 0, 0, 0.5); 598 window = RGBA(0, 0, 0, 0); 599 } 600 601 SliderHandles[hSlider] = new SliderWindow(hSlider, axis, size, thumbSize, padding, colors); 602 return 0; 603 } 604 605 /* The main window creates the child window and has to set the position and size. */ 606 case WM_SIZE: 607 { 608 auto sliderWidth = size + padding + thumbSize; 609 auto sliderHeight = padding; 610 611 if (axis == Axis.vertical) 612 std.algorithm.swap(sliderWidth, sliderHeight); 613 614 MoveWindow(hSlider, 0, 0, sliderWidth, sliderHeight, true); 615 return 0; 616 } 617 618 /* Focus main window, this kills any active child window focus. */ 619 case WM_LBUTTONDOWN: 620 { 621 SetFocus(hwnd); 622 return 0; 623 } 624 625 case WM_KEYDOWN: 626 { 627 if (wParam == VK_ESCAPE) 628 goto case WM_DESTROY; 629 630 return 0; 631 } 632 633 case WM_DESTROY: 634 { 635 PostQuitMessage(0); 636 return 0; 637 } 638 639 default: 640 } 641 642 return DefWindowProc(hwnd, message, wParam, lParam); 643 } 644 645 string sliderClass = "SliderClass"; 646 647 int myWinMain(HINSTANCE hInstance, HINSTANCE hPrevInstance, LPSTR lpCmdLine, int iCmdShow) 648 { 649 string appName = "sliders"; 650 651 HWND hwnd; 652 MSG msg; 653 WNDCLASS wndclass; 654 655 wndclass.style = CS_HREDRAW | CS_VREDRAW; 656 wndclass.lpfnWndProc = &mainWinProc; 657 wndclass.cbClsExtra = 0; 658 wndclass.cbWndExtra = 0; 659 wndclass.hInstance = hInstance; 660 wndclass.hIcon = LoadIcon(null, IDI_APPLICATION); 661 wndclass.hCursor = LoadCursor(null, IDC_ARROW); 662 wndclass.hbrBackground = null; // todo: replace with null, paint bg with cairo 663 wndclass.lpszMenuName = null; 664 wndclass.lpszClassName = appName.toUTF16z; 665 666 if (!RegisterClass(&wndclass)) 667 { 668 MessageBox(null, "This program requires Windows NT!", appName.toUTF16z, MB_ICONERROR); 669 return 0; 670 } 671 672 /* Separate window class for all widgets, in this case only Sliders. */ 673 wndclass.hbrBackground = null; 674 wndclass.lpfnWndProc = &winDispatch; 675 wndclass.cbWndExtra = 0; 676 wndclass.hIcon = null; 677 wndclass.lpszClassName = sliderClass.toUTF16z; 678 679 if (!RegisterClass(&wndclass)) 680 { 681 MessageBox(null, "This program requires Windows NT!", appName.toUTF16z, MB_ICONERROR); 682 return 0; 683 } 684 685 hwnd = CreateWindow(appName.toUTF16z, "sliders example", 686 WS_OVERLAPPEDWINDOW, 687 400, 400, 688 50, 200, 689 null, null, hInstance, null); 690 691 ShowWindow(hwnd, iCmdShow); 692 UpdateWindow(hwnd); 693 694 while (GetMessage(&msg, null, 0, 0)) 695 { 696 TranslateMessage(&msg); 697 DispatchMessage(&msg); 698 } 699 700 return msg.wParam; 701 } 702 703 extern (Windows) 704 int WinMain(HINSTANCE hInstance, HINSTANCE hPrevInstance, LPSTR lpCmdLine, int iCmdShow) 705 { 706 int result; 707 708 try 709 { 710 Runtime.initialize(); 711 result = myWinMain(hInstance, hPrevInstance, lpCmdLine, iCmdShow); 712 Runtime.terminate(); 713 } 714 catch (Throwable o) 715 { 716 MessageBox(null, o.toString().toUTF16z, "Error", MB_OK | MB_ICONEXCLAMATION); 717 result = 0; 718 } 719 720 return result; 721 }