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