1 /*
2  * Theming - Scrollbar control
3  *
4  * Copyright (c) 2015 Mark Harmstone
5  *
6  * This library is free software; you can redistribute it and/or
7  * modify it under the terms of the GNU Lesser General Public
8  * License as published by the Free Software Foundation; either
9  * version 2.1 of the License, or (at your option) any later version.
10  *
11  * This library is distributed in the hope that it will be useful,
12  * but WITHOUT ANY WARRANTY; without even the implied warranty of
13  * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
14  * Lesser General Public License for more details.
15  *
16  * You should have received a copy of the GNU Lesser General Public
17  * License along with this library; if not, write to the Free Software
18  * Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301, USA
19  *
20  */
21 
22 #include "comctl32.h"
23 
24 WINE_DEFAULT_DEBUG_CHANNEL(theme_scroll);
25 
26 /* Minimum size of the thumb in pixels */
27 #define SCROLL_MIN_THUMB 6
28 
29 /* Minimum size of the rectangle between the arrows */
30 #define SCROLL_MIN_RECT  4
31 
32 enum SCROLL_HITTEST
33 {
34     SCROLL_NOWHERE,      /* Outside the scroll bar */
35     SCROLL_TOP_ARROW,    /* Top or left arrow */
36     SCROLL_TOP_RECT,     /* Rectangle between the top arrow and the thumb */
37     SCROLL_THUMB,        /* Thumb rectangle */
38     SCROLL_BOTTOM_RECT,  /* Rectangle between the thumb and the bottom arrow */
39     SCROLL_BOTTOM_ARROW  /* Bottom or right arrow */
40 };
41 
42 static HWND tracking_win = 0;
43 static enum SCROLL_HITTEST tracking_hot_part = SCROLL_NOWHERE;
44 
45 static void calc_thumb_dimensions(unsigned int size, SCROLLINFO *si, unsigned int *thumbpos, unsigned int *thumbsize)
46 {
47     if (size <= SCROLL_MIN_RECT)
48         *thumbpos = *thumbsize = 0;
49     else if (si->nPage > si->nMax - si->nMin)
50         *thumbpos = *thumbsize = 0;
51     else {
52         if (si->nPage > 0) {
53             *thumbsize = MulDiv(size, si->nPage, si->nMax - si->nMin + 1);
54             if (*thumbsize < SCROLL_MIN_THUMB) *thumbsize = SCROLL_MIN_THUMB;
55         }
56         else *thumbsize = GetSystemMetrics(SM_CXVSCROLL);
57 
58         if (size < *thumbsize)
59             *thumbpos = *thumbsize = 0;
60         else {
61             int max = si->nMax - max(si->nPage - 1, 0);
62             size -= *thumbsize;
63             if (si->nMin >= max)
64                 *thumbpos = 0;
65             else
66                 *thumbpos = MulDiv(size, si->nTrackPos - si->nMin, max - si->nMin);
67         }
68     }
69 }
70 
71 static enum SCROLL_HITTEST hit_test(HWND hwnd, HTHEME theme, POINT pt)
72 {
73     RECT r;
74     DWORD style = GetWindowLongW(hwnd, GWL_STYLE);
75     BOOL vertical = style & SBS_VERT;
76     SIZE sz;
77     SCROLLINFO si;
78     unsigned int offset, size, upsize, downsize, thumbpos, thumbsize;
79 
80     GetWindowRect(hwnd, &r);
81     OffsetRect(&r, -r.left, -r.top);
82 
83     if (vertical) {
84         offset = pt.y;
85         size = r.bottom;
86 
87         if (FAILED(GetThemePartSize(theme, NULL, SBP_ARROWBTN, ABS_UPNORMAL, NULL, TS_DRAW, &sz))) {
88             WARN("Could not get up arrow size.\n");
89             upsize = 0;
90         } else
91             upsize = sz.cy;
92 
93         if (FAILED(GetThemePartSize(theme, NULL, SBP_ARROWBTN, ABS_DOWNNORMAL, NULL, TS_DRAW, &sz))) {
94             WARN("Could not get down arrow size.\n");
95             downsize = 0;
96         } else
97             downsize = sz.cy;
98     } else {
99         offset = pt.x;
100         size = r.right;
101 
102         if (FAILED(GetThemePartSize(theme, NULL, SBP_ARROWBTN, ABS_LEFTNORMAL, NULL, TS_DRAW, &sz))) {
103             WARN("Could not get left arrow size.\n");
104             upsize = 0;
105         } else
106             upsize = sz.cx;
107 
108         if (FAILED(GetThemePartSize(theme, NULL, SBP_ARROWBTN, ABS_RIGHTNORMAL, NULL, TS_DRAW, &sz))) {
109             WARN("Could not get right arrow size.\n");
110             downsize = 0;
111         } else
112             downsize = sz.cx;
113     }
114 
115     if (pt.x < 0 || pt.x > r.right || pt.y < 0 || pt.y > r.bottom)
116         return SCROLL_NOWHERE;
117 
118     if (size < SCROLL_MIN_RECT + upsize + downsize)
119         upsize = downsize = (size - SCROLL_MIN_RECT)/2;
120 
121     if (offset < upsize)
122         return SCROLL_TOP_ARROW;
123 
124     if (offset > size - downsize)
125         return SCROLL_BOTTOM_ARROW;
126 
127     si.cbSize = sizeof(si);
128     si.fMask = SIF_ALL;
129     if (!GetScrollInfo(hwnd, SB_CTL, &si)) {
130         WARN("GetScrollInfo failed.\n");
131         return SCROLL_NOWHERE;
132     }
133 
134     calc_thumb_dimensions(size - upsize - downsize, &si, &thumbpos, &thumbsize);
135 
136     if (offset < upsize + thumbpos)
137         return SCROLL_TOP_RECT;
138     else if (offset < upsize + thumbpos + thumbsize)
139         return SCROLL_THUMB;
140     else
141         return SCROLL_BOTTOM_RECT;
142 }
143 
144 static void redraw_part(HWND hwnd, HTHEME theme, enum SCROLL_HITTEST part)
145 {
146     DWORD style = GetWindowLongW(hwnd, GWL_STYLE);
147     BOOL vertical = style & SBS_VERT;
148     SIZE sz;
149     RECT r, partrect;
150     unsigned int size, upsize, downsize;
151 
152     if (part == SCROLL_NOWHERE) { /* redraw everything */
153         InvalidateRect(hwnd, NULL, TRUE);
154         return;
155     }
156 
157     GetWindowRect(hwnd, &r);
158     OffsetRect(&r, -r.left, -r.top);
159 
160     if (vertical) {
161         size = r.bottom;
162 
163         if (FAILED(GetThemePartSize(theme, NULL, SBP_ARROWBTN, ABS_UPNORMAL, NULL, TS_DRAW, &sz))) {
164             WARN("Could not get up arrow size.\n");
165             upsize = 0;
166         } else
167             upsize = sz.cy;
168 
169         if (FAILED(GetThemePartSize(theme, NULL, SBP_ARROWBTN, ABS_DOWNNORMAL, NULL, TS_DRAW, &sz))) {
170             WARN("Could not get down arrow size.\n");
171             downsize = 0;
172         } else
173             downsize = sz.cy;
174     } else {
175         size = r.right;
176 
177         if (FAILED(GetThemePartSize(theme, NULL, SBP_ARROWBTN, ABS_LEFTNORMAL, NULL, TS_DRAW, &sz))) {
178             WARN("Could not get left arrow size.\n");
179             upsize = 0;
180         } else
181             upsize = sz.cx;
182 
183         if (FAILED(GetThemePartSize(theme, NULL, SBP_ARROWBTN, ABS_RIGHTNORMAL, NULL, TS_DRAW, &sz))) {
184             WARN("Could not get right arrow size.\n");
185             downsize = 0;
186         } else
187             downsize = sz.cx;
188     }
189 
190     if (size < SCROLL_MIN_RECT + upsize + downsize)
191         upsize = downsize = (size - SCROLL_MIN_RECT)/2;
192 
193     partrect = r;
194 
195     if (part == SCROLL_TOP_ARROW) {
196         if (vertical)
197             partrect.bottom = partrect.top + upsize;
198         else
199             partrect.right = partrect.left + upsize;
200     } else if (part == SCROLL_BOTTOM_ARROW) {
201         if (vertical)
202             partrect.top = partrect.bottom - downsize;
203         else
204             partrect.left = partrect.right - downsize;
205     } else {
206         unsigned int thumbpos, thumbsize;
207         SCROLLINFO si;
208 
209         si.cbSize = sizeof(si);
210         si.fMask = SIF_ALL;
211         if (!GetScrollInfo(hwnd, SB_CTL, &si)) {
212             WARN("GetScrollInfo failed.\n");
213             return;
214         }
215 
216         calc_thumb_dimensions(size - upsize - downsize, &si, &thumbpos, &thumbsize);
217 
218         if (part == SCROLL_TOP_RECT) {
219             if (vertical) {
220                 partrect.top = r.top + upsize;
221                 partrect.bottom = partrect.top + thumbpos;
222             } else {
223                 partrect.left = r.left + upsize;
224                 partrect.right = partrect.left + thumbpos;
225             }
226         } else if (part == SCROLL_THUMB) {
227             if (vertical) {
228                 partrect.top = r.top + upsize + thumbpos;
229                 partrect.bottom = partrect.top + thumbsize;
230             } else {
231                 partrect.left = r.left + upsize + thumbpos;
232                 partrect.right = partrect.left + thumbsize;
233             }
234         } else if (part == SCROLL_BOTTOM_RECT) {
235             if (vertical) {
236                 partrect.top = r.top + upsize + thumbpos + thumbsize;
237                 partrect.bottom = r.bottom - downsize;
238             } else {
239                 partrect.left = r.left + upsize + thumbpos + thumbsize;
240                 partrect.right = r.right - downsize;
241             }
242         }
243     }
244 
245     InvalidateRect(hwnd, &partrect, TRUE);
246 }
247 
248 static void scroll_event(HWND hwnd, HTHEME theme, UINT msg, POINT pt)
249 {
250     enum SCROLL_HITTEST hittest;
251     TRACKMOUSEEVENT tme;
252 
253     if (GetWindowLongW(hwnd, GWL_STYLE) & (SBS_SIZEGRIP | SBS_SIZEBOX))
254         return;
255 
256     hittest = hit_test(hwnd, theme, pt);
257 
258     switch (msg)
259     {
260         case WM_MOUSEMOVE:
261             hittest = hit_test(hwnd, theme, pt);
262             tracking_win = hwnd;
263             break;
264 
265         case WM_MOUSELEAVE:
266             if (tracking_win == hwnd) {
267                 hittest = SCROLL_NOWHERE;
268             }
269             break;
270     }
271 
272     tme.cbSize = sizeof(tme);
273     tme.dwFlags = TME_QUERY;
274     TrackMouseEvent(&tme);
275 
276     if (!(tme.dwFlags & TME_LEAVE) || tme.hwndTrack != hwnd) {
277         tme.dwFlags = TME_LEAVE;
278         tme.hwndTrack = hwnd;
279         TrackMouseEvent(&tme);
280     }
281 
282     if (tracking_win != hwnd && msg == WM_MOUSELEAVE) {
283         redraw_part(hwnd, theme, SCROLL_NOWHERE);
284         return;
285     }
286 
287     if (tracking_win == hwnd && hittest != tracking_hot_part) {
288         enum SCROLL_HITTEST oldhotpart = tracking_hot_part;
289 
290         tracking_hot_part = hittest;
291 
292         if (hittest != SCROLL_NOWHERE)
293             redraw_part(hwnd, theme, hittest);
294         else
295             tracking_win = 0;
296 
297         if (oldhotpart != SCROLL_NOWHERE)
298             redraw_part(hwnd, theme, oldhotpart);
299     }
300 }
301 
302 static void paint_scrollbar(HWND hwnd, HTHEME theme)
303 {
304     HDC dc;
305     PAINTSTRUCT ps;
306     RECT r;
307     DWORD style = GetWindowLongW(hwnd, GWL_STYLE);
308     BOOL vertical = style & SBS_VERT;
309     BOOL disabled = !IsWindowEnabled(hwnd);
310 
311     GetWindowRect(hwnd, &r);
312     OffsetRect(&r, -r.left, -r.top);
313 
314     dc = BeginPaint(hwnd, &ps);
315 
316     if (style & SBS_SIZEBOX || style & SBS_SIZEGRIP) {
317         int state;
318 
319         if (style & SBS_SIZEBOXTOPLEFTALIGN)
320             state = SZB_TOPLEFTALIGN;
321         else
322             state = SZB_RIGHTALIGN;
323 
324         DrawThemeBackground(theme, dc, SBP_SIZEBOX, state, &r, NULL);
325     } else {
326         SCROLLBARINFO sbi;
327         SCROLLINFO si;
328         unsigned int thumbpos, thumbsize;
329         int uppertrackstate, lowertrackstate, thumbstate;
330         RECT partrect, trackrect;
331         SIZE grippersize;
332 
333         sbi.cbSize = sizeof(sbi);
334         GetScrollBarInfo(hwnd, OBJID_CLIENT, &sbi);
335 
336         si.cbSize = sizeof(si);
337         si.fMask = SIF_ALL;
338         GetScrollInfo(hwnd, SB_CTL, &si);
339 
340         trackrect = r;
341 
342         if (disabled) {
343             uppertrackstate = SCRBS_DISABLED;
344             lowertrackstate = SCRBS_DISABLED;
345             thumbstate = SCRBS_DISABLED;
346         } else {
347             uppertrackstate = SCRBS_NORMAL;
348             lowertrackstate = SCRBS_NORMAL;
349             thumbstate = SCRBS_NORMAL;
350 
351             if (tracking_win == hwnd) {
352                 if (tracking_hot_part == SCROLL_TOP_RECT)
353                     uppertrackstate = SCRBS_HOT;
354                 else if (tracking_hot_part == SCROLL_BOTTOM_RECT)
355                     lowertrackstate = SCRBS_HOT;
356                 else if (tracking_hot_part == SCROLL_THUMB)
357                     thumbstate = SCRBS_HOT;
358             }
359         }
360 
361         if (vertical) {
362             SIZE upsize, downsize;
363             int uparrowstate, downarrowstate;
364 
365             if (disabled) {
366                 uparrowstate = ABS_UPDISABLED;
367                 downarrowstate = ABS_DOWNDISABLED;
368             } else {
369                 uparrowstate = ABS_UPNORMAL;
370                 downarrowstate = ABS_DOWNNORMAL;
371 
372                 if (tracking_win == hwnd) {
373                     if (tracking_hot_part == SCROLL_TOP_ARROW)
374                         uparrowstate = ABS_UPHOT;
375                     else if (tracking_hot_part == SCROLL_BOTTOM_ARROW)
376                         downarrowstate = ABS_DOWNHOT;
377                 }
378             }
379 
380             if (FAILED(GetThemePartSize(theme, dc, SBP_ARROWBTN, uparrowstate, NULL, TS_DRAW, &upsize))) {
381                 WARN("Could not get up arrow size.\n");
382                 return;
383             }
384 
385             if (FAILED(GetThemePartSize(theme, dc, SBP_ARROWBTN, downarrowstate, NULL, TS_DRAW, &downsize))) {
386                 WARN("Could not get down arrow size.\n");
387                 return;
388             }
389 
390             if (r.bottom - r.top - upsize.cy - downsize.cy < SCROLL_MIN_RECT)
391                 upsize.cy = downsize.cy = (r.bottom - r.top - SCROLL_MIN_RECT)/2;
392 
393             partrect = r;
394             partrect.bottom = partrect.top + upsize.cy;
395             DrawThemeBackground(theme, dc, SBP_ARROWBTN, uparrowstate, &partrect, NULL);
396 
397             trackrect.top = partrect.bottom;
398 
399             partrect.bottom = r.bottom;
400             partrect.top = partrect.bottom - downsize.cy;
401             DrawThemeBackground(theme, dc, SBP_ARROWBTN, downarrowstate, &partrect, NULL);
402 
403             trackrect.bottom = partrect.top;
404 
405             calc_thumb_dimensions(trackrect.bottom - trackrect.top, &si, &thumbpos, &thumbsize);
406 
407             if (thumbpos > 0) {
408                 partrect.top = trackrect.top;
409                 partrect.bottom = partrect.top + thumbpos;
410 
411                 DrawThemeBackground(theme, dc, SBP_UPPERTRACKVERT, uppertrackstate, &partrect, NULL);
412             }
413 
414             if (thumbsize > 0) {
415                 partrect.top = trackrect.top + thumbpos;
416                 partrect.bottom = partrect.top + thumbsize;
417 
418                 DrawThemeBackground(theme, dc, SBP_THUMBBTNVERT, thumbstate, &partrect, NULL);
419 
420                 if (SUCCEEDED(GetThemePartSize(theme, dc, SBP_GRIPPERVERT, thumbstate, NULL, TS_DRAW, &grippersize))) {
421                     MARGINS margins;
422 
423                     if (SUCCEEDED(GetThemeMargins(theme, dc, SBP_THUMBBTNVERT, thumbstate, TMT_CONTENTMARGINS, &partrect, &margins))) {
424                         if (grippersize.cy <= (thumbsize - margins.cyTopHeight - margins.cyBottomHeight))
425                             DrawThemeBackground(theme, dc, SBP_GRIPPERVERT, thumbstate, &partrect, NULL);
426                     }
427                 }
428             }
429 
430             if (thumbpos + thumbsize < trackrect.bottom - trackrect.top) {
431                 partrect.bottom = trackrect.bottom;
432                 partrect.top = trackrect.top + thumbsize + thumbpos;
433 
434                 DrawThemeBackground(theme, dc, SBP_LOWERTRACKVERT, lowertrackstate, &partrect, NULL);
435             }
436         } else {
437             SIZE leftsize, rightsize;
438             int leftarrowstate, rightarrowstate;
439 
440             if (disabled) {
441                 leftarrowstate = ABS_LEFTDISABLED;
442                 rightarrowstate = ABS_RIGHTDISABLED;
443             } else {
444                 leftarrowstate = ABS_LEFTNORMAL;
445                 rightarrowstate = ABS_RIGHTNORMAL;
446 
447                 if (tracking_win == hwnd) {
448                     if (tracking_hot_part == SCROLL_TOP_ARROW)
449                         leftarrowstate = ABS_LEFTHOT;
450                     else if (tracking_hot_part == SCROLL_BOTTOM_ARROW)
451                         rightarrowstate = ABS_RIGHTHOT;
452                 }
453             }
454 
455             if (FAILED(GetThemePartSize(theme, dc, SBP_ARROWBTN, leftarrowstate, NULL, TS_DRAW, &leftsize))) {
456                 WARN("Could not get left arrow size.\n");
457                 return;
458             }
459 
460             if (FAILED(GetThemePartSize(theme, dc, SBP_ARROWBTN, rightarrowstate, NULL, TS_DRAW, &rightsize))) {
461                 WARN("Could not get right arrow size.\n");
462                 return;
463             }
464 
465             if (r.right - r.left - leftsize.cx - rightsize.cx < SCROLL_MIN_RECT)
466                 leftsize.cx = rightsize.cx = (r.right - r.left - SCROLL_MIN_RECT)/2;
467 
468             partrect = r;
469             partrect.right = partrect.left + leftsize.cx;
470             DrawThemeBackground(theme, dc, SBP_ARROWBTN, leftarrowstate, &partrect, NULL);
471 
472             trackrect.left = partrect.right;
473 
474             partrect.right = r.right;
475             partrect.left = partrect.right - rightsize.cx;
476             DrawThemeBackground(theme, dc, SBP_ARROWBTN, rightarrowstate, &partrect, NULL);
477 
478             trackrect.right = partrect.left;
479 
480             calc_thumb_dimensions(trackrect.right - trackrect.left, &si, &thumbpos, &thumbsize);
481 
482             if (thumbpos > 0) {
483                 partrect.left = trackrect.left;
484                 partrect.right = partrect.left + thumbpos;
485 
486                 DrawThemeBackground(theme, dc, SBP_UPPERTRACKHORZ, uppertrackstate, &partrect, NULL);
487             }
488 
489             if (thumbsize > 0) {
490                 partrect.left = trackrect.left + thumbpos;
491                 partrect.right = partrect.left + thumbsize;
492 
493                 DrawThemeBackground(theme, dc, SBP_THUMBBTNHORZ, thumbstate, &partrect, NULL);
494 
495                 if (SUCCEEDED(GetThemePartSize(theme, dc, SBP_GRIPPERHORZ, thumbstate, NULL, TS_DRAW, &grippersize))) {
496                     MARGINS margins;
497 
498                     if (SUCCEEDED(GetThemeMargins(theme, dc, SBP_THUMBBTNHORZ, thumbstate, TMT_CONTENTMARGINS, &partrect, &margins))) {
499                         if (grippersize.cx <= (thumbsize - margins.cxLeftWidth - margins.cxRightWidth))
500                             DrawThemeBackground(theme, dc, SBP_GRIPPERHORZ, thumbstate, &partrect, NULL);
501                     }
502                 }
503             }
504 
505             if (thumbpos + thumbsize < trackrect.right - trackrect.left) {
506                 partrect.right = trackrect.right;
507                 partrect.left = trackrect.left + thumbsize + thumbpos;
508 
509                 DrawThemeBackground(theme, dc, SBP_LOWERTRACKHORZ, lowertrackstate, &partrect, NULL);
510             }
511         }
512     }
513 
514     EndPaint(hwnd, &ps);
515 }
516 
517 LRESULT CALLBACK THEMING_ScrollbarSubclassProc (HWND hwnd, UINT msg,
518                                                 WPARAM wParam, LPARAM lParam,
519                                                 ULONG_PTR dwRefData)
520 {
521     const WCHAR* themeClass = WC_SCROLLBARW;
522     HTHEME theme;
523     LRESULT result;
524     POINT pt;
525 
526     TRACE("(%p, 0x%x, %lu, %lu, %lu)\n", hwnd, msg, wParam, lParam, dwRefData);
527 
528     switch (msg) {
529         case WM_CREATE:
530             result = THEMING_CallOriginalClass(hwnd, msg, wParam, lParam);
531             OpenThemeData(hwnd, themeClass);
532             return result;
533 
534         case WM_DESTROY:
535             theme = GetWindowTheme(hwnd);
536             CloseThemeData(theme);
537             return THEMING_CallOriginalClass(hwnd, msg, wParam, lParam);
538 
539         case WM_THEMECHANGED:
540             theme = GetWindowTheme(hwnd);
541             CloseThemeData(theme);
542             OpenThemeData(hwnd, themeClass);
543             break;
544 
545         case WM_SYSCOLORCHANGE:
546             theme = GetWindowTheme(hwnd);
547             if (!theme) return THEMING_CallOriginalClass(hwnd, msg, wParam, lParam);
548             /* Do nothing. When themed, a WM_THEMECHANGED will be received, too,
549              * which will do the repaint. */
550             break;
551 
552         case WM_PAINT:
553             theme = GetWindowTheme(hwnd);
554             if (!theme) return THEMING_CallOriginalClass(hwnd, msg, wParam, lParam);
555 
556             paint_scrollbar(hwnd, theme);
557             break;
558 
559         case WM_MOUSEMOVE:
560         case WM_MOUSELEAVE:
561             theme = GetWindowTheme(hwnd);
562             if (!theme) return THEMING_CallOriginalClass(hwnd, msg, wParam, lParam);
563 
564             pt.x = (short)LOWORD(lParam);
565             pt.y = (short)HIWORD(lParam);
566             scroll_event(hwnd, theme, msg, pt);
567             break;
568 
569         default:
570             return THEMING_CallOriginalClass(hwnd, msg, wParam, lParam);
571     }
572 
573     return 0;
574 }
575