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     /// true if full scroll range is visible, and no need of scrolling at all
286     @property bool fullRangeVisible() {
287         return _pageSize >= _maxValue - _minValue;
288     }
289 
290     private bool calcButtonSizes(int availableSize, ref int spaceBackSize, ref int spaceForwardSize, ref int indicatorSize) {
291         int dv = _maxValue - _minValue;
292         if (_pageSize >= dv) {
293             // full size
294             spaceBackSize = spaceForwardSize = 0;
295             indicatorSize = availableSize;
296             return false;
297         }
298         if (dv < 0)
299             dv = 0;
300         indicatorSize = dv ? _pageSize * availableSize / dv : _minIndicatorSize;
301         if (indicatorSize < _minIndicatorSize)
302             indicatorSize = _minIndicatorSize;
303         if (indicatorSize >= availableSize) {
304             // full size
305             spaceBackSize = spaceForwardSize = 0;
306             indicatorSize = availableSize;
307             return false;
308         }
309         int spaceLeft = availableSize - indicatorSize;
310         int topv = _position - _minValue;
311         int bottomv = _position + _pageSize - _minValue;
312         if (topv < 0)
313             topv = 0;
314         if (bottomv > dv)
315             bottomv = dv;
316         bottomv = dv - bottomv;
317         spaceBackSize = cast(int)(cast(long)spaceLeft * topv / (topv + bottomv));
318         spaceForwardSize = spaceLeft - spaceBackSize;
319         return true;
320     }
321 
322     /// returns scrollbar orientation (Vertical, Horizontal)
323     override @property Orientation orientation() { return _orientation; }
324     /// sets scrollbar orientation
325     override @property AbstractSlider orientation(Orientation value) {
326         if (_orientation != value) {
327             _orientation = value;
328             updateDrawableIds();
329             requestLayout();
330         }
331         return this;
332     }
333 
334     void updateDrawableIds() {
335         _btnBack.drawableId = style.customDrawableId(_orientation == Orientation.Vertical ? ATTR_SCROLLBAR_BUTTON_UP : ATTR_SCROLLBAR_BUTTON_LEFT);
336         _btnForward.drawableId = style.customDrawableId(_orientation == Orientation.Vertical ? ATTR_SCROLLBAR_BUTTON_DOWN : ATTR_SCROLLBAR_BUTTON_RIGHT);
337         _indicator.drawableId = style.customDrawableId(_orientation == Orientation.Vertical ? ATTR_SCROLLBAR_INDICATOR_VERTICAL : ATTR_SCROLLBAR_INDICATOR_HORIZONTAL);
338     }
339 
340     /// handle theme change: e.g. reload some themed resources
341     override void onThemeChanged() {
342         super.onThemeChanged();
343         updateDrawableIds();
344     }
345 
346     /// set string property value, for ML loaders
347     override bool setStringProperty(string name, string value) {
348         if (name.equal("orientation")) {
349             if (value.equal("Vertical") || value.equal("vertical"))
350                 orientation = Orientation.Vertical;
351             else
352                 orientation = Orientation.Horizontal;
353             return true;
354         }
355         return super.setStringProperty(name, value);
356     }
357 
358 
359     /// empty parameter list constructor - for usage by factory
360     this() {
361         this(null, Orientation.Vertical);
362     }
363     /// create with ID parameter
364     this(string ID, Orientation orient = Orientation.Vertical) {
365         super(ID);
366         styleId = STYLE_SCROLLBAR;
367         _orientation = orient;
368         _btnBack = new ImageButton("BACK", style.customDrawableId(_orientation == Orientation.Vertical ? ATTR_SCROLLBAR_BUTTON_UP : ATTR_SCROLLBAR_BUTTON_LEFT));
369         _btnForward = new ImageButton("FORWARD", style.customDrawableId(_orientation == Orientation.Vertical ? ATTR_SCROLLBAR_BUTTON_DOWN : ATTR_SCROLLBAR_BUTTON_RIGHT));
370         _pageUp = new PageScrollButton("PAGE_UP");
371         _pageDown = new PageScrollButton("PAGE_DOWN");
372         _btnBack.styleId = STYLE_SCROLLBAR_BUTTON_TRANSPARENT;
373         _btnForward.styleId = STYLE_SCROLLBAR_BUTTON_TRANSPARENT;
374         _indicator = new SliderButton(style.customDrawableId(_orientation == Orientation.Vertical ? ATTR_SCROLLBAR_INDICATOR_VERTICAL : ATTR_SCROLLBAR_INDICATOR_HORIZONTAL));
375         addChild(_btnBack);
376         addChild(_btnForward);
377         addChild(_indicator);
378         addChild(_pageUp);
379         addChild(_pageDown);
380         _btnBack.focusable = false;
381         _btnForward.focusable = false;
382         _indicator.focusable = false;
383         _pageUp.focusable = false;
384         _pageDown.focusable = false;
385         _btnBack.click = &onClick;
386         _btnForward.click = &onClick;
387         _pageUp.click = &onClick;
388         _pageDown.click = &onClick;
389     }
390 
391     override void measure(int parentWidth, int parentHeight) {
392         Point sz;
393         _btnBack.measure(parentWidth, parentHeight);
394         _btnForward.measure(parentWidth, parentHeight);
395         _indicator.measure(parentWidth, parentHeight);
396         _pageUp.measure(parentWidth, parentHeight);
397         _pageDown.measure(parentWidth, parentHeight);
398         _btnSize = _btnBack.measuredWidth;
399         _minIndicatorSize = _orientation == Orientation.Vertical ? _indicator.measuredHeight : _indicator.measuredWidth;
400         if (_btnSize < _minIndicatorSize)
401             _btnSize = _minIndicatorSize;
402         if (_btnSize < _btnForward.measuredWidth)
403             _btnSize = _btnForward.measuredWidth;
404         if (_btnSize < _btnForward.measuredHeight)
405             _btnSize = _btnForward.measuredHeight;
406         if (_btnSize < _btnBack.measuredHeight)
407             _btnSize = _btnBack.measuredHeight;
408         static if (BACKEND_GUI) {
409             if (_btnSize < 16)
410                 _btnSize = 16;
411         }
412         if (_orientation == Orientation.Vertical) {
413             // vertical
414             sz.x = _btnSize;
415             sz.y = _btnSize * 5; // min height
416         } else {
417             // horizontal
418             sz.y = _btnSize;
419             sz.x = _btnSize * 5; // min height
420         }
421         measuredContent(parentWidth, parentHeight, sz.x, sz.y);
422     }
423 
424     override protected void onPositionChanged() {
425         if (!needLayout)
426             layoutButtons();
427     }
428 
429     /// hide controls when scroll is not possible
430     protected void updateState() {
431         bool canScroll = _maxValue - _minValue > _pageSize;
432         if (canScroll) {
433             _btnBack.setState(State.Enabled);
434             _btnForward.setState(State.Enabled);
435             _indicator.visibility = Visibility.Visible;
436             if (_position > _minValue)
437                 _pageUp.visibility = Visibility.Visible;
438             if (_position < _maxValue)
439                 _pageDown.visibility = Visibility.Visible;
440         } else {
441             _btnBack.resetState(State.Enabled);
442             _btnForward.resetState(State.Enabled);
443             _indicator.visibility = Visibility.Gone;
444             _pageUp.visibility = Visibility.Gone;
445             _pageDown.visibility = Visibility.Gone;
446         }
447         cancelLayout();
448     }
449 
450     override void cancelLayout() {
451         _btnBack.cancelLayout();
452         _btnForward.cancelLayout();
453         _indicator.cancelLayout();
454         _pageUp.cancelLayout();
455         _pageDown.cancelLayout();
456         super.cancelLayout();
457     }
458 
459     protected void layoutButtons() {
460         Rect irc = _scrollArea;
461         if (_orientation == Orientation.Vertical) {
462             // vertical
463             int spaceBackSize, spaceForwardSize, indicatorSize;
464             bool indicatorVisible = calcButtonSizes(_scrollArea.height, spaceBackSize, spaceForwardSize, indicatorSize);
465             irc.top += spaceBackSize;
466             irc.bottom -= spaceForwardSize;
467             layoutButtons(irc);
468         } else {
469             // horizontal
470             int spaceBackSize, spaceForwardSize, indicatorSize;
471             bool indicatorVisible = calcButtonSizes(_scrollArea.width, spaceBackSize, spaceForwardSize, indicatorSize);
472             irc.left += spaceBackSize;
473             irc.right -= spaceForwardSize;
474             layoutButtons(irc);
475         }
476         updateState();
477         cancelLayout();
478     }
479 
480     protected void layoutButtons(Rect irc) {
481         Rect r;
482         _indicator.visibility = Visibility.Visible;
483         if (_orientation == Orientation.Vertical) {
484             _indicator.layout(irc);
485             if (_scrollArea.top < irc.top) {
486                 r = _scrollArea;
487                 r.bottom = irc.top;
488                 _pageUp.visibility = Visibility.Visible;
489                 _pageUp.layout(r);
490             } else {
491                 _pageUp.visibility = Visibility.Invisible;
492             }
493             if (_scrollArea.bottom > irc.bottom) {
494                 r = _scrollArea;
495                 r.top = irc.bottom;
496                 _pageDown.visibility = Visibility.Visible;
497                 _pageDown.layout(r);
498             } else {
499                 _pageDown.visibility = Visibility.Invisible;
500             }
501         } else {
502             _indicator.layout(irc);
503             if (_scrollArea.left < irc.left) {
504                 r = _scrollArea;
505                 r.right = irc.left;
506                 _pageUp.visibility = Visibility.Visible;
507                 _pageUp.layout(r);
508             } else {
509                 _pageUp.visibility = Visibility.Invisible;
510             }
511             if (_scrollArea.right > irc.right) {
512                 r = _scrollArea;
513                 r.left = irc.right;
514                 _pageDown.visibility = Visibility.Visible;
515                 _pageDown.layout(r);
516             } else {
517                 _pageDown.visibility = Visibility.Invisible;
518             }
519         }
520     }
521 
522     override void layout(Rect rc) {
523         _needLayout = false;
524         applyMargins(rc);
525         applyPadding(rc);
526         Rect r;
527         if (_orientation == Orientation.Vertical) {
528             // vertical
529             // buttons
530             int backbtnpos = rc.top + _btnSize;
531             int fwdbtnpos = rc.bottom - _btnSize;
532             r = rc;
533             r.bottom = backbtnpos;
534             _btnBack.layout(r);
535             r = rc;
536             r.top = fwdbtnpos;
537             _btnForward.layout(r);
538             // indicator
539             r = rc;
540             r.top = backbtnpos;
541             r.bottom = fwdbtnpos;
542             _scrollArea = r;
543         } else {
544             // horizontal
545             int backbtnpos = rc.left + _btnSize;
546             int fwdbtnpos = rc.right - _btnSize;
547             r = rc;
548             r.right = backbtnpos;
549             _btnBack.layout(r);
550             r = rc;
551             r.left = fwdbtnpos;
552             _btnForward.layout(r);
553             // indicator
554             r = rc;
555             r.left = backbtnpos;
556             r.right = fwdbtnpos;
557             _scrollArea = r;
558         }
559         layoutButtons();
560         _pos = rc;
561     }
562 
563     override bool onClick(Widget source) {
564         Log.d("Scrollbar.onClick ", source.id);
565         if (source.compareId("BACK"))
566             return sendScrollEvent(ScrollAction.LineUp, position);
567         if (source.compareId("FORWARD"))
568             return sendScrollEvent(ScrollAction.LineDown, position);
569         if (source.compareId("PAGE_UP"))
570             return sendScrollEvent(ScrollAction.PageUp, position);
571         if (source.compareId("PAGE_DOWN"))
572             return sendScrollEvent(ScrollAction.PageDown, position);
573         return true;
574     }
575 
576     /// handle mouse wheel events
577     override bool onMouseEvent(MouseEvent event) {
578         if (visibility != Visibility.Visible)
579             return false;
580         if (event.action == MouseAction.Wheel) {
581             int delta = event.wheelDelta;
582             if (delta > 0)
583                 sendScrollEvent(ScrollAction.LineUp, position);
584             else if (delta < 0)
585                 sendScrollEvent(ScrollAction.LineDown, position);
586             return true;
587         }
588         return true;
589         //return super.onMouseEvent(event);
590     }
591 
592     /// Draw widget at its position to buffer
593     override void onDraw(DrawBuf buf) {
594         if (visibility != Visibility.Visible && !buf.isClippedOut(_pos))
595             return;
596         super.onDraw(buf);
597         Rect rc = _pos;
598         applyMargins(rc);
599         applyPadding(rc);
600         auto saver = ClipRectSaver(buf, rc, alpha);
601         _btnForward.onDraw(buf);
602         _btnBack.onDraw(buf);
603         _pageUp.onDraw(buf);
604         _pageDown.onDraw(buf);
605         _indicator.onDraw(buf);
606     }
607 }
608 
609 /// scroll bar - either vertical or horizontal
610 class SliderWidget : AbstractSlider, OnClickHandler {
611     protected SliderButton _indicator;
612     protected PageScrollButton _pageUp;
613     protected PageScrollButton _pageDown;
614     protected Rect _scrollArea;
615     protected int _btnSize;
616     protected int _minIndicatorSize;
617 
618     class PageScrollButton : Widget {
619         this(string ID) {
620             super(ID);
621             styleId = STYLE_PAGE_SCROLL;
622             trackHover = true;
623             clickable = true;
624         }
625     }
626 
627     class SliderButton : ImageButton {
628         Point _dragStart;
629         int _dragStartPosition;
630         bool _dragging;
631         Rect _dragStartRect;
632 
633         this(string resourceId) {
634             super("SLIDER", resourceId);
635             styleId = STYLE_SCROLLBAR_BUTTON;
636             trackHover = true;
637         }
638 
639         /// process mouse event; return true if event is processed by widget.
640         override bool onMouseEvent(MouseEvent event) {
641             // support onClick
642             if (event.action == MouseAction.ButtonDown && event.button == MouseButton.Left) {
643                 setState(State.Pressed);
644                 _dragging = true;
645                 _dragStart.x = event.x;
646                 _dragStart.y = event.y;
647                 _dragStartPosition = _position;
648                 _dragStartRect = _pos;
649                 sendScrollEvent(ScrollAction.SliderPressed, _position);
650                 return true;
651             }
652             if (event.action == MouseAction.FocusOut && _dragging) {
653                 debug(scrollbar) Log.d("ScrollBar slider dragging - FocusOut");
654                 return true;
655             }
656             if (event.action == MouseAction.FocusIn && _dragging) {
657                 debug(scrollbar) Log.d("ScrollBar slider dragging - FocusIn");
658                 return true;
659             }
660             if (event.action == MouseAction.Move && _dragging) {
661                 int delta = _orientation == Orientation.Vertical ? event.y - _dragStart.y : event.x - _dragStart.x;
662                 debug(scrollbar) Log.d("ScrollBar slider dragging - Move delta=", delta);
663                 Rect rc = _dragStartRect;
664                 int offset;
665                 int space;
666                 if (_orientation == Orientation.Vertical) {
667                     rc.top += delta;
668                     rc.bottom += delta;
669                     if (rc.top < _scrollArea.top) {
670                         rc.top = _scrollArea.top;
671                         rc.bottom = _scrollArea.top + _dragStartRect.height;
672                     } else if (rc.bottom > _scrollArea.bottom) {
673                         rc.top = _scrollArea.bottom - _dragStartRect.height;
674                         rc.bottom = _scrollArea.bottom;
675                     }
676                     offset = rc.top - _scrollArea.top;
677                     space = _scrollArea.height - rc.height;
678                 } else {
679                     rc.left += delta;
680                     rc.right += delta;
681                     if (rc.left < _scrollArea.left) {
682                         rc.left = _scrollArea.left;
683                         rc.right = _scrollArea.left + _dragStartRect.width;
684                     } else if (rc.right > _scrollArea.right) {
685                         rc.left = _scrollArea.right - _dragStartRect.width;
686                         rc.right = _scrollArea.right;
687                     }
688                     offset = rc.left - _scrollArea.left;
689                     space = _scrollArea.width - rc.width;
690                 }
691                 layoutButtons(rc);
692                 //_pos = rc;
693                 int position = cast(int)(space > 0 ? _minValue + cast(long)offset * (_maxValue - _minValue - _pageSize) / space : 0);
694                 invalidate();
695                 onIndicatorDragging(_dragStartPosition, position);
696                 return true;
697             }
698             if (event.action == MouseAction.ButtonUp && event.button == MouseButton.Left) {
699                 resetState(State.Pressed);
700                 if (_dragging) {
701                     sendScrollEvent(ScrollAction.SliderReleased, _position);
702                     _dragging = false;
703                 }
704                 return true;
705             }
706             if (event.action == MouseAction.Move && trackHover) {
707                 if (!(state & State.Hovered)) {
708                     debug(scrollbar) Log.d("Hover ", id);
709                     setState(State.Hovered);
710                 }
711                 return true;
712             }
713             if (event.action == MouseAction.Leave && trackHover) {
714                 debug(scrollbar) Log.d("Leave ", id);
715                 resetState(State.Hovered);
716                 return true;
717             }
718             if (event.action == MouseAction.Cancel && trackHover) {
719                 debug(scrollbar) Log.d("Cancel ? trackHover", id);
720                 resetState(State.Hovered);
721                 resetState(State.Pressed);
722                 _dragging = false;
723                 return true;
724             }
725             if (event.action == MouseAction.Cancel) {
726                 debug(scrollbar) Log.d("SliderButton.onMouseEvent event.action == MouseAction.Cancel");
727                 resetState(State.Pressed);
728                 _dragging = false;
729                 return true;
730             }
731             return false;
732         }
733 
734     }
735 
736     protected bool onIndicatorDragging(int initialPosition, int currentPosition) {
737         _position = currentPosition;
738         return sendScrollEvent(ScrollAction.SliderMoved, currentPosition);
739     }
740 
741     private bool calcButtonSizes(int availableSize, ref int spaceBackSize, ref int spaceForwardSize, ref int indicatorSize) {
742         int dv = _maxValue - _minValue;
743         if (_pageSize >= dv) {
744             // full size
745             spaceBackSize = spaceForwardSize = 0;
746             indicatorSize = availableSize;
747             return false;
748         }
749         if (dv < 0)
750             dv = 0;
751         indicatorSize = dv ? _pageSize * availableSize / dv : _minIndicatorSize;
752         if (indicatorSize < _minIndicatorSize)
753             indicatorSize = _minIndicatorSize;
754         if (indicatorSize >= availableSize) {
755             // full size
756             spaceBackSize = spaceForwardSize = 0;
757             indicatorSize = availableSize;
758             return false;
759         }
760         int spaceLeft = availableSize - indicatorSize;
761         int topv = _position - _minValue;
762         int bottomv = _position + _pageSize - _minValue;
763         if (topv < 0)
764             topv = 0;
765         if (bottomv > dv)
766             bottomv = dv;
767         bottomv = dv - bottomv;
768         spaceBackSize = cast(int)(cast(long)spaceLeft * topv / (topv + bottomv));
769         spaceForwardSize = spaceLeft - spaceBackSize;
770         return true;
771     }
772 
773     /// returns scrollbar orientation (Vertical, Horizontal)
774     override @property Orientation orientation() { return _orientation; }
775     /// sets scrollbar orientation
776     override @property AbstractSlider orientation(Orientation value) {
777         if (_orientation != value) {
778             _orientation = value;
779             _indicator.drawableId = style.customDrawableId(_orientation == Orientation.Vertical ? ATTR_SCROLLBAR_INDICATOR_VERTICAL : ATTR_SCROLLBAR_INDICATOR_HORIZONTAL);
780             requestLayout();
781         }
782         return this;
783     }
784 
785     /// set string property value, for ML loaders
786     override bool setStringProperty(string name, string value) {
787         if (name.equal("orientation")) {
788             if (value.equal("Vertical") || value.equal("vertical"))
789                 orientation = Orientation.Vertical;
790             else
791                 orientation = Orientation.Horizontal;
792             return true;
793         }
794         return super.setStringProperty(name, value);
795     }
796 
797 
798     /// empty parameter list constructor - for usage by factory
799     this() {
800         this(null, Orientation.Horizontal);
801     }
802     /// create with ID parameter
803     this(string ID, Orientation orient = Orientation.Horizontal) {
804         super(ID);
805         styleId = STYLE_SLIDER;
806         _orientation = orient;
807         _pageSize = 1;
808         _pageUp = new PageScrollButton("PAGE_UP");
809         _pageDown = new PageScrollButton("PAGE_DOWN");
810         _indicator = new SliderButton(style.customDrawableId(_orientation == Orientation.Vertical ? ATTR_SCROLLBAR_INDICATOR_VERTICAL : ATTR_SCROLLBAR_INDICATOR_HORIZONTAL));
811         addChild(_indicator);
812         addChild(_pageUp);
813         addChild(_pageDown);
814         _indicator.focusable = false;
815         _pageUp.focusable = false;
816         _pageDown.focusable = false;
817         _pageUp.click = &onClick;
818         _pageDown.click = &onClick;
819     }
820 
821     override void measure(int parentWidth, int parentHeight) {
822         Point sz;
823         _indicator.measure(parentWidth, parentHeight);
824         _pageUp.measure(parentWidth, parentHeight);
825         _pageDown.measure(parentWidth, parentHeight);
826         _minIndicatorSize = _orientation == Orientation.Vertical ? _indicator.measuredHeight : _indicator.measuredWidth;
827         _btnSize = _minIndicatorSize;
828         if (_btnSize < _minIndicatorSize)
829             _btnSize = _minIndicatorSize;
830         static if (BACKEND_GUI) {
831             if (_btnSize < 16)
832                 _btnSize = 16;
833         }
834         if (_orientation == Orientation.Vertical) {
835             // vertical
836             sz.x = _btnSize;
837             sz.y = _btnSize * 5; // min height
838         } else {
839             // horizontal
840             sz.y = _btnSize;
841             sz.x = _btnSize * 5; // min height
842         }
843         measuredContent(parentWidth, parentHeight, sz.x, sz.y);
844     }
845 
846     override protected void onPositionChanged() {
847         if (!needLayout)
848             layoutButtons();
849     }
850 
851     /// hide controls when scroll is not possible
852     protected void updateState() {
853         bool canScroll = _maxValue - _minValue > _pageSize;
854         if (canScroll) {
855             _indicator.visibility = Visibility.Visible;
856             _pageUp.visibility = Visibility.Visible;
857             _pageDown.visibility = Visibility.Visible;
858         } else {
859             _indicator.visibility = Visibility.Gone;
860             _pageUp.visibility = Visibility.Gone;
861             _pageDown.visibility = Visibility.Gone;
862         }
863         cancelLayout();
864     }
865 
866     override void cancelLayout() {
867         _indicator.cancelLayout();
868         _pageUp.cancelLayout();
869         _pageDown.cancelLayout();
870         super.cancelLayout();
871     }
872 
873     protected void layoutButtons() {
874         Rect irc = _scrollArea;
875         if (_orientation == Orientation.Vertical) {
876             // vertical
877             int spaceBackSize, spaceForwardSize, indicatorSize;
878             bool indicatorVisible = calcButtonSizes(_scrollArea.height, spaceBackSize, spaceForwardSize, indicatorSize);
879             irc.top += spaceBackSize;
880             irc.bottom -= spaceForwardSize;
881             layoutButtons(irc);
882         } else {
883             // horizontal
884             int spaceBackSize, spaceForwardSize, indicatorSize;
885             bool indicatorVisible = calcButtonSizes(_scrollArea.width, spaceBackSize, spaceForwardSize, indicatorSize);
886             irc.left += spaceBackSize;
887             irc.right -= spaceForwardSize;
888             layoutButtons(irc);
889         }
890         updateState();
891         cancelLayout();
892     }
893 
894     protected void layoutButtons(Rect irc) {
895         Rect r;
896         _indicator.visibility = Visibility.Visible;
897         if (_orientation == Orientation.Vertical) {
898             _indicator.layout(irc);
899             if (_scrollArea.top < irc.top) {
900                 r = _scrollArea;
901                 r.bottom = irc.top;
902                 _pageUp.layout(r);
903                 _pageUp.visibility = Visibility.Visible;
904             } else {
905                 _pageUp.visibility = Visibility.Invisible;
906             }
907             if (_scrollArea.bottom > irc.bottom) {
908                 r = _scrollArea;
909                 r.top = irc.bottom;
910                 _pageDown.layout(r);
911                 _pageDown.visibility = Visibility.Visible;
912             } else {
913                 _pageDown.visibility = Visibility.Invisible;
914             }
915         } else {
916             _indicator.layout(irc);
917             if (_scrollArea.left < irc.left) {
918                 r = _scrollArea;
919                 r.right = irc.left;
920                 _pageUp.layout(r);
921                 _pageUp.visibility = Visibility.Visible;
922             } else {
923                 _pageUp.visibility = Visibility.Invisible;
924             }
925             if (_scrollArea.right > irc.right) {
926                 r = _scrollArea;
927                 r.left = irc.right;
928                 _pageDown.layout(r);
929                 _pageDown.visibility = Visibility.Visible;
930             } else {
931                 _pageDown.visibility = Visibility.Invisible;
932             }
933         }
934     }
935 
936     override void layout(Rect rc) {
937         _needLayout = false;
938         applyMargins(rc);
939         applyPadding(rc);
940         Rect r;
941         if (_orientation == Orientation.Vertical) {
942             // vertical
943             // buttons
944             // indicator
945             r = rc;
946             _scrollArea = r;
947         } else {
948             // horizontal
949             // indicator
950             r = rc;
951             _scrollArea = r;
952         }
953         layoutButtons();
954         _pos = rc;
955     }
956 
957     override bool onClick(Widget source) {
958         Log.d("Scrollbar.onClick ", source.id);
959         if (source.compareId("PAGE_UP"))
960             return sendScrollEvent(ScrollAction.PageUp, position);
961         if (source.compareId("PAGE_DOWN"))
962             return sendScrollEvent(ScrollAction.PageDown, position);
963         return true;
964     }
965 
966     /// handle mouse wheel events
967     override bool onMouseEvent(MouseEvent event) {
968         if (visibility != Visibility.Visible)
969             return false;
970         if (event.action == MouseAction.Wheel) {
971             int delta = event.wheelDelta;
972             if (delta > 0)
973                 sendScrollEvent(ScrollAction.LineUp, position);
974             else if (delta < 0)
975                 sendScrollEvent(ScrollAction.LineDown, position);
976             return true;
977         }
978         return super.onMouseEvent(event);
979     }
980 
981     /// Draw widget at its position to buffer
982     override void onDraw(DrawBuf buf) {
983         if (visibility != Visibility.Visible && !buf.isClippedOut(_pos))
984             return;
985         Rect rc = _pos;
986         applyMargins(rc);
987         auto saver = ClipRectSaver(buf, rc, alpha);
988         DrawableRef bg = backgroundDrawable;
989         if (!bg.isNull) {
990             Rect r = rc;
991             if (_orientation == Orientation.Vertical) {
992                 int dw = bg.width;
993                 r.left += (rc.width - dw)/2;
994                 r.right = r.left + dw;
995             } else {
996                 int dw = bg.height;
997                 r.top += (rc.height - dw)/2;
998                 r.bottom = r.top + dw;
999             }
1000             bg.drawTo(buf, r, state);
1001         }
1002         applyPadding(rc);
1003         if (state & State.Focused) {
1004             rc.expand(FOCUS_RECT_PADDING, FOCUS_RECT_PADDING);
1005             drawFocusRect(buf, rc);
1006         }
1007         _needDraw = false;
1008         _pageUp.onDraw(buf);
1009         _pageDown.onDraw(buf);
1010         _indicator.onDraw(buf);
1011     }
1012 }
1013