1 // Written in the D programming language.
2 
3 /**
4 This module contains simple scrollbar-like controls implementation.
5 
6 ScrollBar - scrollbar control
7 
8 SliderWidget - slider control
9 
10 
11 
12 Synopsis:
13 
14 ----
15 import dlangui.widgets.scrollbar;
16 
17 ----
18 
19 Copyright: Vadim Lopatin, 2014
20 License:   Boost License 1.0
21 Authors:   Vadim Lopatin, coolreader.org@gmail.com
22 */
23 module dlangui.widgets.scrollbar;
24 
25 
26 import dlangui.widgets.widget;
27 import dlangui.widgets.layouts;
28 import dlangui.widgets.controls;
29 import dlangui.core.events;
30 import dlangui.core.stdaction;
31 
32 private import std.algorithm;
33 private import std.conv : to;
34 private import std.utf : toUTF32;
35 
36 /// scroll event handler interface
37 interface OnScrollHandler {
38     /// handle scroll event
39     bool onScrollEvent(AbstractSlider source, ScrollEvent event);
40 }
41 
42 /// base class for widgets like scrollbars and sliders
43 class AbstractSlider : WidgetGroup {
44     protected int _minValue = 0;
45     protected int _maxValue = 100;
46     protected int _pageSize = 30;
47     protected int _position = 20;
48 
49     /// create with ID parameter
50     this(string ID) {
51         super(ID);
52     }
53 
54     /// scroll event listeners
55     Signal!OnScrollHandler scrollEvent;
56 
57     /// returns slider position
58     @property int position() const { return _position; }
59     /// sets new slider position
60     @property AbstractSlider position(int newPosition) { 
61         if (_position != newPosition) {
62             _position = newPosition;
63             onPositionChanged();
64         }
65         return this;
66     }
67     protected void onPositionChanged() {
68         requestLayout();
69     }
70     /// returns slider range min value
71     @property int minValue() const { return _minValue; }
72     /// sets slider range min value
73     @property AbstractSlider minValue(int v) { _minValue = v; return this; }
74     /// returns slider range max value
75     @property int maxValue() const { return _maxValue; }
76     /// sets slider range max value
77     @property AbstractSlider maxValue(int v) { _maxValue = v; return this; }
78 
79 
80 
81     /// page size (visible area size)
82     @property int pageSize() const { return _pageSize; }
83     /// set page size (visible area size)
84     @property AbstractSlider pageSize(int size) {
85         if (_pageSize != size) {
86             _pageSize = size;
87             requestLayout();
88         }
89         return this;
90     }
91 
92     /// set int property value, for ML loaders
93     //mixin(generatePropertySettersMethodOverride("setIntProperty", "int",
94     //      "minValue", "maxValue", "pageSize", "position"));
95     /// set int property value, for ML loaders
96     override bool setIntProperty(string name, int value) {
97         if (name.equal("orientation")) { // use same value for all sides
98             orientation = cast(Orientation)value;
99             return true;
100         }
101         mixin(generatePropertySetters("minValue", "maxValue", "pageSize", "position"));
102         return super.setIntProperty(name, value);
103     }
104 
105     /// set new range (min and max values for slider)
106     AbstractSlider setRange(int min, int max) {
107         if (_minValue != min || _maxValue != max) {
108             _minValue = min;
109             _maxValue = max;
110             requestLayout();
111         }
112         return this;
113     }
114 
115     bool sendScrollEvent(ScrollAction action) {
116         return sendScrollEvent(action, _position);
117     }
118 
119     bool sendScrollEvent(ScrollAction action, int position) {
120         if (!scrollEvent.assigned)
121             return false;
122         ScrollEvent event = new ScrollEvent(action, _minValue, _maxValue, _pageSize, position);
123         bool res = scrollEvent(this, event);
124         if (event.positionChanged) {
125             _position = event.position;
126             if (_position > _maxValue)
127                 _position = _maxValue;
128             if (_position < _minValue)
129                 _position = _minValue;
130             onPositionChanged();
131         }
132         return true;
133     }
134 
135     protected Orientation _orientation = Orientation.Vertical;
136     /// returns scrollbar orientation (Vertical, Horizontal)
137     @property Orientation orientation() { return _orientation; }
138     /// sets scrollbar orientation
139     @property AbstractSlider orientation(Orientation value) { 
140         if (_orientation != value) {
141             _orientation = value; 
142             requestLayout(); 
143         }
144         return this; 
145     }
146 
147 }
148 
149 /// scroll bar - either vertical or horizontal
150 class ScrollBar : AbstractSlider, OnClickHandler {
151     protected ImageButton _btnBack;
152     protected ImageButton _btnForward;
153     protected SliderButton _indicator;
154     protected PageScrollButton _pageUp;
155     protected PageScrollButton _pageDown;
156     protected Rect _scrollArea;
157     protected int _btnSize;
158     protected int _minIndicatorSize;
159 
160 
161 
162     class PageScrollButton : Widget {
163         this(string ID) {
164             super(ID);
165             styleId = STYLE_PAGE_SCROLL;
166             trackHover = true;
167             clickable = true;
168         }
169     }
170 
171     class SliderButton : ImageButton {
172         Point _dragStart;
173         int _dragStartPosition;
174         bool _dragging;
175         Rect _dragStartRect;
176 
177         this(string resourceId) {
178             super("SLIDER", resourceId);
179             styleId = STYLE_SCROLLBAR_BUTTON;
180             trackHover = true;
181         }
182 
183         /// process mouse event; return true if event is processed by widget.
184         override bool onMouseEvent(MouseEvent event) {
185             // support onClick
186             if (event.action == MouseAction.ButtonDown && event.button == MouseButton.Left) {
187                 setState(State.Pressed);
188                 _dragging = true;
189                 _dragStart.x = event.x;
190                 _dragStart.y = event.y;
191                 _dragStartPosition = _position;
192                 _dragStartRect = _pos;
193                 sendScrollEvent(ScrollAction.SliderPressed, _position);
194                 return true;
195             }
196             if (event.action == MouseAction.FocusOut && _dragging) {
197                 debug(scrollbar) Log.d("ScrollBar slider dragging - FocusOut");
198                 return true;
199             }
200             if (event.action == MouseAction.FocusIn && _dragging) {
201                 debug(scrollbar) Log.d("ScrollBar slider dragging - FocusIn");
202                 return true;
203             }
204             if (event.action == MouseAction.Move && _dragging) {
205                 int delta = _orientation == Orientation.Vertical ? event.y - _dragStart.y : event.x - _dragStart.x;
206                 debug(scrollbar) Log.d("ScrollBar slider dragging - Move delta=", delta);
207                 Rect rc = _dragStartRect;
208                 int offset;
209                 int space;
210                 if (_orientation == Orientation.Vertical) {
211                     rc.top += delta;
212                     rc.bottom += delta;
213                     if (rc.top < _scrollArea.top) {
214                         rc.top = _scrollArea.top;
215                         rc.bottom = _scrollArea.top + _dragStartRect.height;
216                     } else if (rc.bottom > _scrollArea.bottom) {
217                         rc.top = _scrollArea.bottom - _dragStartRect.height;
218                         rc.bottom = _scrollArea.bottom;
219                     }
220                     offset = rc.top - _scrollArea.top;
221                     space = _scrollArea.height - rc.height;
222                 } else {
223                     rc.left += delta;
224                     rc.right += delta;
225                     if (rc.left < _scrollArea.left) {
226                         rc.left = _scrollArea.left;
227                         rc.right = _scrollArea.left + _dragStartRect.width;
228                     } else if (rc.right > _scrollArea.right) {
229                         rc.left = _scrollArea.right - _dragStartRect.width;
230                         rc.right = _scrollArea.right;
231                     }
232                     offset = rc.left - _scrollArea.left;
233                     space = _scrollArea.width - rc.width;
234                 }
235                 layoutButtons(rc);
236                 //_pos = rc;
237                 int position = cast(int)(space > 0 ? _minValue + cast(long)offset * (_maxValue - _minValue - _pageSize) / space : 0);
238                 invalidate();
239                 onIndicatorDragging(_dragStartPosition, position);
240                 return true;
241             }
242             if (event.action == MouseAction.ButtonUp && event.button == MouseButton.Left) {
243                 resetState(State.Pressed);
244                 if (_dragging) {
245                     sendScrollEvent(ScrollAction.SliderReleased, _position);
246                     _dragging = false;
247                 }
248                 return true;
249             }
250             if (event.action == MouseAction.Move && trackHover) {
251                 if (!(state & State.Hovered)) {
252                     debug(scrollbar) Log.d("Hover ", id);
253                     setState(State.Hovered);
254                 }
255                 return true;
256             }
257             if (event.action == MouseAction.Leave && trackHover) {
258                 debug(scrollbar) Log.d("Leave ", id);
259                 resetState(State.Hovered);
260                 return true;
261             }
262             if (event.action == MouseAction.Cancel && trackHover) {
263                 debug(scrollbar) Log.d("Cancel ? trackHover", id);
264                 resetState(State.Hovered);
265                 resetState(State.Pressed);
266                 _dragging = false;
267                 return true;
268             }
269             if (event.action == MouseAction.Cancel) {
270                 debug(scrollbar) Log.d("SliderButton.onMouseEvent event.action == MouseAction.Cancel");
271                 resetState(State.Pressed);
272                 _dragging = false;
273                 return true;
274             }
275             return false;
276         }
277 
278     }
279 
280     protected bool onIndicatorDragging(int initialPosition, int currentPosition) {
281         _position = currentPosition;
282         return sendScrollEvent(ScrollAction.SliderMoved, currentPosition);
283     }
284 
285     private bool calcButtonSizes(int availableSize, ref int spaceBackSize, ref int spaceForwardSize, ref int indicatorSize) {
286         int dv = _maxValue - _minValue;
287         if (_pageSize >= dv) {
288             // full size
289             spaceBackSize = spaceForwardSize = 0;
290             indicatorSize = availableSize;
291             return false;
292         }
293         if (dv < 0)
294             dv = 0;
295         indicatorSize = dv ? _pageSize * availableSize / dv : _minIndicatorSize;
296         if (indicatorSize < _minIndicatorSize)
297             indicatorSize = _minIndicatorSize;
298         if (indicatorSize >= availableSize) {
299             // full size
300             spaceBackSize = spaceForwardSize = 0;
301             indicatorSize = availableSize;
302             return false;
303         }
304         int spaceLeft = availableSize - indicatorSize;
305         int topv = _position - _minValue;
306         int bottomv = _position + _pageSize - _minValue;
307         if (topv < 0)
308             topv = 0;
309         if (bottomv > dv)
310             bottomv = dv;
311         bottomv = dv - bottomv;
312         spaceBackSize = cast(int)(cast(long)spaceLeft * topv / (topv + bottomv));
313         spaceForwardSize = spaceLeft - spaceBackSize;
314         return true;
315     }
316 
317     /// returns scrollbar orientation (Vertical, Horizontal)
318     override @property Orientation orientation() { return _orientation; }
319     /// sets scrollbar orientation
320     override @property AbstractSlider orientation(Orientation value) { 
321         if (_orientation != value) {
322             _orientation = value;
323             _btnBack.drawableId = style.customDrawableId(_orientation == Orientation.Vertical ? ATTR_SCROLLBAR_BUTTON_UP : ATTR_SCROLLBAR_BUTTON_LEFT);
324             _btnForward.drawableId = style.customDrawableId(_orientation == Orientation.Vertical ? ATTR_SCROLLBAR_BUTTON_DOWN : ATTR_SCROLLBAR_BUTTON_RIGHT);
325             _indicator.drawableId = style.customDrawableId(_orientation == Orientation.Vertical ? ATTR_SCROLLBAR_INDICATOR_VERTICAL : ATTR_SCROLLBAR_INDICATOR_HORIZONTAL);
326             requestLayout();
327         }
328         return this; 
329     }
330 
331     /// set string property value, for ML loaders
332     override bool setStringProperty(string name, string value) {
333         if (name.equal("orientation")) {
334             if (value.equal("Vertical") || value.equal("vertical"))
335                 orientation = Orientation.Vertical;
336             else
337                 orientation = Orientation.Horizontal;
338             return true;
339         }
340         return super.setStringProperty(name, value);
341     }
342 
343 
344     /// empty parameter list constructor - for usage by factory
345     this() {
346         this(null, Orientation.Vertical);
347     }
348     /// create with ID parameter
349     this(string ID, Orientation orient = Orientation.Vertical) {
350         super(ID);
351         styleId = STYLE_SCROLLBAR;
352         _orientation = orient;
353         _btnBack = new ImageButton("BACK", style.customDrawableId(_orientation == Orientation.Vertical ? ATTR_SCROLLBAR_BUTTON_UP : ATTR_SCROLLBAR_BUTTON_LEFT));
354         _btnForward = new ImageButton("FORWARD", style.customDrawableId(_orientation == Orientation.Vertical ? ATTR_SCROLLBAR_BUTTON_DOWN : ATTR_SCROLLBAR_BUTTON_RIGHT));
355         _pageUp = new PageScrollButton("PAGE_UP");
356         _pageDown = new PageScrollButton("PAGE_DOWN");
357         _btnBack.styleId = STYLE_SCROLLBAR_BUTTON_TRANSPARENT;
358         _btnForward.styleId = STYLE_SCROLLBAR_BUTTON_TRANSPARENT;
359         _indicator = new SliderButton(style.customDrawableId(_orientation == Orientation.Vertical ? ATTR_SCROLLBAR_INDICATOR_VERTICAL : ATTR_SCROLLBAR_INDICATOR_HORIZONTAL));
360         addChild(_btnBack);
361         addChild(_btnForward);
362         addChild(_indicator);
363         addChild(_pageUp);
364         addChild(_pageDown);
365         _btnBack.focusable = false;
366         _btnForward.focusable = false;
367         _indicator.focusable = false;
368         _pageUp.focusable = false;
369         _pageDown.focusable = false;
370         _btnBack.click = &onClick;
371         _btnForward.click = &onClick;
372         _pageUp.click = &onClick;
373         _pageDown.click = &onClick;
374     }
375 
376     override void measure(int parentWidth, int parentHeight) { 
377         Point sz;
378         _btnBack.measure(parentWidth, parentHeight);
379         _btnForward.measure(parentWidth, parentHeight);
380         _indicator.measure(parentWidth, parentHeight);
381         _pageUp.measure(parentWidth, parentHeight);
382         _pageDown.measure(parentWidth, parentHeight);
383         _btnSize = _btnBack.measuredWidth;
384         _minIndicatorSize = _orientation == Orientation.Vertical ? _indicator.measuredHeight : _indicator.measuredWidth;
385         if (_btnSize < _minIndicatorSize)
386             _btnSize = _minIndicatorSize;
387         if (_btnSize < _btnForward.measuredWidth)
388             _btnSize = _btnForward.measuredWidth;
389         if (_btnSize < _btnForward.measuredHeight)
390             _btnSize = _btnForward.measuredHeight;
391         if (_btnSize < _btnBack.measuredHeight)
392             _btnSize = _btnBack.measuredHeight;
393         static if (BACKEND_GUI) {
394             if (_btnSize < 16)
395                 _btnSize = 16;
396         }
397         if (_orientation == Orientation.Vertical) {
398             // vertical
399             sz.x = _btnSize;
400             sz.y = _btnSize * 5; // min height
401         } else {
402             // horizontal
403             sz.y = _btnSize;
404             sz.x = _btnSize * 5; // min height
405         }
406         measuredContent(parentWidth, parentHeight, sz.x, sz.y);
407     }
408 
409     override protected void onPositionChanged() {
410         if (!needLayout)
411             layoutButtons();
412     }
413 
414     /// hide controls when scroll is not possible
415     protected void updateState() {
416         bool canScroll = _maxValue - _minValue > _pageSize;
417         if (canScroll) {
418             _btnBack.setState(State.Enabled);
419             _btnForward.setState(State.Enabled);
420             _indicator.visibility = Visibility.Visible;
421             _pageUp.visibility = Visibility.Visible;
422             _pageDown.visibility = Visibility.Visible;
423         } else {
424             _btnBack.resetState(State.Enabled);
425             _btnForward.resetState(State.Enabled);
426             _indicator.visibility = Visibility.Gone;
427             _pageUp.visibility = Visibility.Gone;
428             _pageDown.visibility = Visibility.Gone;
429         }
430         cancelLayout();
431     }
432 
433     override void cancelLayout() {
434         _btnBack.cancelLayout();
435         _btnForward.cancelLayout();
436         _indicator.cancelLayout();
437         _pageUp.cancelLayout();
438         _pageDown.cancelLayout();
439         super.cancelLayout();
440     }
441 
442     protected void layoutButtons() {
443         Rect irc = _scrollArea;
444         if (_orientation == Orientation.Vertical) {
445             // vertical
446             int spaceBackSize, spaceForwardSize, indicatorSize;
447             bool indicatorVisible = calcButtonSizes(_scrollArea.height, spaceBackSize, spaceForwardSize, indicatorSize);
448             irc.top += spaceBackSize;
449             irc.bottom -= spaceForwardSize;
450             layoutButtons(irc);
451         } else {
452             // horizontal
453             int spaceBackSize, spaceForwardSize, indicatorSize;
454             bool indicatorVisible = calcButtonSizes(_scrollArea.width, spaceBackSize, spaceForwardSize, indicatorSize);
455             irc.left += spaceBackSize;
456             irc.right -= spaceForwardSize;
457             layoutButtons(irc);
458         }
459         updateState();
460         cancelLayout();
461     }
462 
463     protected void layoutButtons(Rect irc) {
464         Rect r;
465         _indicator.visibility = Visibility.Visible;
466         if (_orientation == Orientation.Vertical) {
467             _indicator.layout(irc);
468             if (_scrollArea.top < irc.top) {
469                 r = _scrollArea;
470                 r.bottom = irc.top;
471                 _pageUp.layout(r);
472                 _pageUp.visibility = Visibility.Visible;
473             } else {
474                 _pageUp.visibility = Visibility.Invisible;
475             }
476             if (_scrollArea.bottom > irc.bottom) {
477                 r = _scrollArea;
478                 r.top = irc.bottom;
479                 _pageDown.layout(r);
480                 _pageDown.visibility = Visibility.Visible;
481             } else {
482                 _pageDown.visibility = Visibility.Invisible;
483             }
484         } else {
485             _indicator.layout(irc);
486             if (_scrollArea.left < irc.left) {
487                 r = _scrollArea;
488                 r.right = irc.left;
489                 _pageUp.layout(r);
490                 _pageUp.visibility = Visibility.Visible;
491             } else {
492                 _pageUp.visibility = Visibility.Invisible;
493             }
494             if (_scrollArea.right > irc.right) {
495                 r = _scrollArea;
496                 r.left = irc.right;
497                 _pageDown.layout(r);
498                 _pageDown.visibility = Visibility.Visible;
499             } else {
500                 _pageDown.visibility = Visibility.Invisible;
501             }
502         }
503     }
504 
505     override void layout(Rect rc) {
506         _needLayout = false;
507         applyMargins(rc);
508         applyPadding(rc);
509         Rect r;
510         if (_orientation == Orientation.Vertical) {
511             // vertical
512             // buttons
513             int backbtnpos = rc.top + _btnSize;
514             int fwdbtnpos = rc.bottom - _btnSize;
515             r = rc;
516             r.bottom = backbtnpos;
517             _btnBack.layout(r);
518             r = rc;
519             r.top = fwdbtnpos;
520             _btnForward.layout(r);
521             // indicator
522             r = rc;
523             r.top = backbtnpos;
524             r.bottom = fwdbtnpos;
525             _scrollArea = r;
526         } else {
527             // horizontal
528             int backbtnpos = rc.left + _btnSize;
529             int fwdbtnpos = rc.right - _btnSize;
530             r = rc;
531             r.right = backbtnpos;
532             _btnBack.layout(r);
533             r = rc;
534             r.left = fwdbtnpos;
535             _btnForward.layout(r);
536             // indicator
537             r = rc;
538             r.left = backbtnpos;
539             r.right = fwdbtnpos;
540             _scrollArea = r;
541         }
542         layoutButtons();
543         _pos = rc;
544     }
545 
546     override bool onClick(Widget source) {
547         Log.d("Scrollbar.onClick ", source.id);
548         if (source.compareId("BACK"))
549             return sendScrollEvent(ScrollAction.LineUp, position);
550         if (source.compareId("FORWARD"))
551             return sendScrollEvent(ScrollAction.LineDown, position);
552         if (source.compareId("PAGE_UP"))
553             return sendScrollEvent(ScrollAction.PageUp, position);
554         if (source.compareId("PAGE_DOWN"))
555             return sendScrollEvent(ScrollAction.PageDown, position);
556         return true;
557     }
558 
559     /// handle mouse wheel events
560     override bool onMouseEvent(MouseEvent event) {
561         if (visibility != Visibility.Visible)
562             return false;
563         if (event.action == MouseAction.Wheel) {
564             int delta = event.wheelDelta;
565             if (delta > 0)
566                 sendScrollEvent(ScrollAction.LineUp, position);
567             else if (delta < 0)
568                 sendScrollEvent(ScrollAction.LineDown, position);
569             return true;
570         }
571         return true;
572         //return super.onMouseEvent(event);
573     }
574 
575     /// Draw widget at its position to buffer
576     override void onDraw(DrawBuf buf) {
577         if (visibility != Visibility.Visible && !buf.isClippedOut(_pos))
578             return;
579         super.onDraw(buf);
580         Rect rc = _pos;
581         applyMargins(rc);
582         applyPadding(rc);
583         auto saver = ClipRectSaver(buf, rc, alpha);
584         _btnForward.onDraw(buf);
585         _btnBack.onDraw(buf);
586         _pageUp.onDraw(buf);
587         _pageDown.onDraw(buf);
588         _indicator.onDraw(buf);
589     }
590 }
591 
592 /// scroll bar - either vertical or horizontal
593 class SliderWidget : AbstractSlider, OnClickHandler {
594     protected SliderButton _indicator;
595     protected PageScrollButton _pageUp;
596     protected PageScrollButton _pageDown;
597     protected Rect _scrollArea;
598     protected int _btnSize;
599     protected int _minIndicatorSize;
600 
601     class PageScrollButton : Widget {
602         this(string ID) {
603             super(ID);
604             styleId = STYLE_PAGE_SCROLL;
605             trackHover = true;
606             clickable = true;
607         }
608     }
609 
610     class SliderButton : ImageButton {
611         Point _dragStart;
612         int _dragStartPosition;
613         bool _dragging;
614         Rect _dragStartRect;
615 
616         this(string resourceId) {
617             super("SLIDER", resourceId);
618             styleId = STYLE_SCROLLBAR_BUTTON;
619             trackHover = true;
620         }
621 
622         /// process mouse event; return true if event is processed by widget.
623         override bool onMouseEvent(MouseEvent event) {
624             // support onClick
625             if (event.action == MouseAction.ButtonDown && event.button == MouseButton.Left) {
626                 setState(State.Pressed);
627                 _dragging = true;
628                 _dragStart.x = event.x;
629                 _dragStart.y = event.y;
630                 _dragStartPosition = _position;
631                 _dragStartRect = _pos;
632                 sendScrollEvent(ScrollAction.SliderPressed, _position);
633                 return true;
634             }
635             if (event.action == MouseAction.FocusOut && _dragging) {
636                 debug(scrollbar) Log.d("ScrollBar slider dragging - FocusOut");
637                 return true;
638             }
639             if (event.action == MouseAction.FocusIn && _dragging) {
640                 debug(scrollbar) Log.d("ScrollBar slider dragging - FocusIn");
641                 return true;
642             }
643             if (event.action == MouseAction.Move && _dragging) {
644                 int delta = _orientation == Orientation.Vertical ? event.y - _dragStart.y : event.x - _dragStart.x;
645                 debug(scrollbar) Log.d("ScrollBar slider dragging - Move delta=", delta);
646                 Rect rc = _dragStartRect;
647                 int offset;
648                 int space;
649                 if (_orientation == Orientation.Vertical) {
650                     rc.top += delta;
651                     rc.bottom += delta;
652                     if (rc.top < _scrollArea.top) {
653                         rc.top = _scrollArea.top;
654                         rc.bottom = _scrollArea.top + _dragStartRect.height;
655                     } else if (rc.bottom > _scrollArea.bottom) {
656                         rc.top = _scrollArea.bottom - _dragStartRect.height;
657                         rc.bottom = _scrollArea.bottom;
658                     }
659                     offset = rc.top - _scrollArea.top;
660                     space = _scrollArea.height - rc.height;
661                 } else {
662                     rc.left += delta;
663                     rc.right += delta;
664                     if (rc.left < _scrollArea.left) {
665                         rc.left = _scrollArea.left;
666                         rc.right = _scrollArea.left + _dragStartRect.width;
667                     } else if (rc.right > _scrollArea.right) {
668                         rc.left = _scrollArea.right - _dragStartRect.width;
669                         rc.right = _scrollArea.right;
670                     }
671                     offset = rc.left - _scrollArea.left;
672                     space = _scrollArea.width - rc.width;
673                 }
674                 layoutButtons(rc);
675                 //_pos = rc;
676                 int position = cast(int)(space > 0 ? _minValue + cast(long)offset * (_maxValue - _minValue - _pageSize) / space : 0);
677                 invalidate();
678                 onIndicatorDragging(_dragStartPosition, position);
679                 return true;
680             }
681             if (event.action == MouseAction.ButtonUp && event.button == MouseButton.Left) {
682                 resetState(State.Pressed);
683                 if (_dragging) {
684                     sendScrollEvent(ScrollAction.SliderReleased, _position);
685                     _dragging = false;
686                 }
687                 return true;
688             }
689             if (event.action == MouseAction.Move && trackHover) {
690                 if (!(state & State.Hovered)) {
691                     debug(scrollbar) Log.d("Hover ", id);
692                     setState(State.Hovered);
693                 }
694                 return true;
695             }
696             if (event.action == MouseAction.Leave && trackHover) {
697                 debug(scrollbar) Log.d("Leave ", id);
698                 resetState(State.Hovered);
699                 return true;
700             }
701             if (event.action == MouseAction.Cancel && trackHover) {
702                 debug(scrollbar) Log.d("Cancel ? trackHover", id);
703                 resetState(State.Hovered);
704                 resetState(State.Pressed);
705                 _dragging = false;
706                 return true;
707             }
708             if (event.action == MouseAction.Cancel) {
709                 debug(scrollbar) Log.d("SliderButton.onMouseEvent event.action == MouseAction.Cancel");
710                 resetState(State.Pressed);
711                 _dragging = false;
712                 return true;
713             }
714             return false;
715         }
716 
717     }
718 
719     protected bool onIndicatorDragging(int initialPosition, int currentPosition) {
720         _position = currentPosition;
721         return sendScrollEvent(ScrollAction.SliderMoved, currentPosition);
722     }
723 
724     private bool calcButtonSizes(int availableSize, ref int spaceBackSize, ref int spaceForwardSize, ref int indicatorSize) {
725         int dv = _maxValue - _minValue;
726         if (_pageSize >= dv) {
727             // full size
728             spaceBackSize = spaceForwardSize = 0;
729             indicatorSize = availableSize;
730             return false;
731         }
732         if (dv < 0)
733             dv = 0;
734         indicatorSize = dv ? _pageSize * availableSize / dv : _minIndicatorSize;
735         if (indicatorSize < _minIndicatorSize)
736             indicatorSize = _minIndicatorSize;
737         if (indicatorSize >= availableSize) {
738             // full size
739             spaceBackSize = spaceForwardSize = 0;
740             indicatorSize = availableSize;
741             return false;
742         }
743         int spaceLeft = availableSize - indicatorSize;
744         int topv = _position - _minValue;
745         int bottomv = _position + _pageSize - _minValue;
746         if (topv < 0)
747             topv = 0;
748         if (bottomv > dv)
749             bottomv = dv;
750         bottomv = dv - bottomv;
751         spaceBackSize = cast(int)(cast(long)spaceLeft * topv / (topv + bottomv));
752         spaceForwardSize = spaceLeft - spaceBackSize;
753         return true;
754     }
755 
756     /// returns scrollbar orientation (Vertical, Horizontal)
757     override @property Orientation orientation() { return _orientation; }
758     /// sets scrollbar orientation
759     override @property AbstractSlider orientation(Orientation value) { 
760         if (_orientation != value) {
761             _orientation = value; 
762             _indicator.drawableId = style.customDrawableId(_orientation == Orientation.Vertical ? ATTR_SCROLLBAR_INDICATOR_VERTICAL : ATTR_SCROLLBAR_INDICATOR_HORIZONTAL);
763             requestLayout(); 
764         }
765         return this; 
766     }
767 
768     /// set string property value, for ML loaders
769     override bool setStringProperty(string name, string value) {
770         if (name.equal("orientation")) {
771             if (value.equal("Vertical") || value.equal("vertical"))
772                 orientation = Orientation.Vertical;
773             else
774                 orientation = Orientation.Horizontal;
775             return true;
776         }
777         return super.setStringProperty(name, value);
778     }
779 
780 
781     /// empty parameter list constructor - for usage by factory
782     this() {
783         this(null, Orientation.Horizontal);
784     }
785     /// create with ID parameter
786     this(string ID, Orientation orient = Orientation.Horizontal) {
787         super(ID);
788         styleId = STYLE_SLIDER;
789         _orientation = orient;
790         _pageSize = 1;
791         _pageUp = new PageScrollButton("PAGE_UP");
792         _pageDown = new PageScrollButton("PAGE_DOWN");
793         _indicator = new SliderButton(style.customDrawableId(_orientation == Orientation.Vertical ? ATTR_SCROLLBAR_INDICATOR_VERTICAL : ATTR_SCROLLBAR_INDICATOR_HORIZONTAL));
794         addChild(_indicator);
795         addChild(_pageUp);
796         addChild(_pageDown);
797         _indicator.focusable = false;
798         _pageUp.focusable = false;
799         _pageDown.focusable = false;
800         _pageUp.click = &onClick;
801         _pageDown.click = &onClick;
802     }
803 
804     override void measure(int parentWidth, int parentHeight) { 
805         Point sz;
806         _indicator.measure(parentWidth, parentHeight);
807         _pageUp.measure(parentWidth, parentHeight);
808         _pageDown.measure(parentWidth, parentHeight);
809         _minIndicatorSize = _orientation == Orientation.Vertical ? _indicator.measuredHeight : _indicator.measuredWidth;
810         _btnSize = _minIndicatorSize;
811         if (_btnSize < _minIndicatorSize)
812             _btnSize = _minIndicatorSize;
813         static if (BACKEND_GUI) {
814             if (_btnSize < 16)
815                 _btnSize = 16;
816         }
817         if (_orientation == Orientation.Vertical) {
818             // vertical
819             sz.x = _btnSize;
820             sz.y = _btnSize * 5; // min height
821         } else {
822             // horizontal
823             sz.y = _btnSize;
824             sz.x = _btnSize * 5; // min height
825         }
826         measuredContent(parentWidth, parentHeight, sz.x, sz.y);
827     }
828 
829     override protected void onPositionChanged() {
830         if (!needLayout)
831             layoutButtons();
832     }
833 
834     /// hide controls when scroll is not possible
835     protected void updateState() {
836         bool canScroll = _maxValue - _minValue > _pageSize;
837         if (canScroll) {
838             _indicator.visibility = Visibility.Visible;
839             _pageUp.visibility = Visibility.Visible;
840             _pageDown.visibility = Visibility.Visible;
841         } else {
842             _indicator.visibility = Visibility.Gone;
843             _pageUp.visibility = Visibility.Gone;
844             _pageDown.visibility = Visibility.Gone;
845         }
846         cancelLayout();
847     }
848 
849     override void cancelLayout() {
850         _indicator.cancelLayout();
851         _pageUp.cancelLayout();
852         _pageDown.cancelLayout();
853         super.cancelLayout();
854     }
855 
856     protected void layoutButtons() {
857         Rect irc = _scrollArea;
858         if (_orientation == Orientation.Vertical) {
859             // vertical
860             int spaceBackSize, spaceForwardSize, indicatorSize;
861             bool indicatorVisible = calcButtonSizes(_scrollArea.height, spaceBackSize, spaceForwardSize, indicatorSize);
862             irc.top += spaceBackSize;
863             irc.bottom -= spaceForwardSize;
864             layoutButtons(irc);
865         } else {
866             // horizontal
867             int spaceBackSize, spaceForwardSize, indicatorSize;
868             bool indicatorVisible = calcButtonSizes(_scrollArea.width, spaceBackSize, spaceForwardSize, indicatorSize);
869             irc.left += spaceBackSize;
870             irc.right -= spaceForwardSize;
871             layoutButtons(irc);
872         }
873         updateState();
874         cancelLayout();
875     }
876 
877     protected void layoutButtons(Rect irc) {
878         Rect r;
879         _indicator.visibility = Visibility.Visible;
880         if (_orientation == Orientation.Vertical) {
881             _indicator.layout(irc);
882             if (_scrollArea.top < irc.top) {
883                 r = _scrollArea;
884                 r.bottom = irc.top;
885                 _pageUp.layout(r);
886                 _pageUp.visibility = Visibility.Visible;
887             } else {
888                 _pageUp.visibility = Visibility.Invisible;
889             }
890             if (_scrollArea.bottom > irc.bottom) {
891                 r = _scrollArea;
892                 r.top = irc.bottom;
893                 _pageDown.layout(r);
894                 _pageDown.visibility = Visibility.Visible;
895             } else {
896                 _pageDown.visibility = Visibility.Invisible;
897             }
898         } else {
899             _indicator.layout(irc);
900             if (_scrollArea.left < irc.left) {
901                 r = _scrollArea;
902                 r.right = irc.left;
903                 _pageUp.layout(r);
904                 _pageUp.visibility = Visibility.Visible;
905             } else {
906                 _pageUp.visibility = Visibility.Invisible;
907             }
908             if (_scrollArea.right > irc.right) {
909                 r = _scrollArea;
910                 r.left = irc.right;
911                 _pageDown.layout(r);
912                 _pageDown.visibility = Visibility.Visible;
913             } else {
914                 _pageDown.visibility = Visibility.Invisible;
915             }
916         }
917     }
918 
919     override void layout(Rect rc) {
920         _needLayout = false;
921         applyMargins(rc);
922         applyPadding(rc);
923         Rect r;
924         if (_orientation == Orientation.Vertical) {
925             // vertical
926             // buttons
927             // indicator
928             r = rc;
929             _scrollArea = r;
930         } else {
931             // horizontal
932             // indicator
933             r = rc;
934             _scrollArea = r;
935         }
936         layoutButtons();
937         _pos = rc;
938     }
939 
940     override bool onClick(Widget source) {
941         Log.d("Scrollbar.onClick ", source.id);
942         if (source.compareId("PAGE_UP"))
943             return sendScrollEvent(ScrollAction.PageUp, position);
944         if (source.compareId("PAGE_DOWN"))
945             return sendScrollEvent(ScrollAction.PageDown, position);
946         return true;
947     }
948 
949     /// handle mouse wheel events
950     override bool onMouseEvent(MouseEvent event) {
951         if (visibility != Visibility.Visible)
952             return false;
953         if (event.action == MouseAction.Wheel) {
954             int delta = event.wheelDelta;
955             if (delta > 0)
956                 sendScrollEvent(ScrollAction.LineUp, position);
957             else if (delta < 0)
958                 sendScrollEvent(ScrollAction.LineDown, position);
959             return true;
960         }
961         return super.onMouseEvent(event);
962     }
963 
964     /// Draw widget at its position to buffer
965     override void onDraw(DrawBuf buf) {
966         if (visibility != Visibility.Visible && !buf.isClippedOut(_pos))
967             return;
968         Rect rc = _pos;
969         applyMargins(rc);
970         auto saver = ClipRectSaver(buf, rc, alpha);
971         DrawableRef bg = backgroundDrawable;
972         if (!bg.isNull) {
973             Rect r = rc;
974             if (_orientation == Orientation.Vertical) {
975                 int dw = bg.width;
976                 r.left += (rc.width - dw)/2;
977                 r.right = r.left + dw;
978             } else {
979                 int dw = bg.height;
980                 r.top += (rc.height - dw)/2;
981                 r.bottom = r.top + dw;
982             }
983             bg.drawTo(buf, r, state);
984         }
985         applyPadding(rc);
986         if (state & State.Focused) {
987             rc.expand(FOCUS_RECT_PADDING, FOCUS_RECT_PADDING);
988             drawFocusRect(buf, rc);
989         }
990         _needDraw = false;
991         _pageUp.onDraw(buf);
992         _pageDown.onDraw(buf);
993         _indicator.onDraw(buf);
994     }
995 }
996