1 // Written in the D programming language.
2 
3 /**
4 
5 This module contains base implementation of scrolling capabilities for widgets
6 
7 
8 ScrollWidgetBase - abstract scrollable widget (used as a base for other widgets with scrolling)
9 
10 ScrollWidget - widget which can scroll its content (directly usable class)
11 
12 
13 Synopsis:
14 
15 ----
16 import dlangui.widgets.scroll;
17 
18 // Scroll view example
19 ScrollWidget scroll = new ScrollWidget("SCROLL1");
20 scroll.layoutWidth(FILL_PARENT).layoutHeight(FILL_PARENT);
21 WidgetGroup scrollContent = new VerticalLayout("CONTENT");
22 scrollContent.layoutWidth(FILL_PARENT).layoutHeight(FILL_PARENT);
23 
24 TableLayout table2 = new TableLayout("TABLE2");
25 table2.colCount = 2;
26 // headers
27 table2.addChild((new TextWidget(null, "Parameter Name"d)).alignment(Align.Right | Align.VCenter));
28 table2.addChild((new TextWidget(null, "Edit Box to edit parameter"d)).alignment(Align.Left | Align.VCenter));
29 // row 1
30 table2.addChild((new TextWidget(null, "Parameter 1 name"d)).alignment(Align.Right | Align.VCenter));
31 table2.addChild((new EditLine("edit1", "Text 1"d)).layoutWidth(FILL_PARENT));
32 // row 2
33 table2.addChild((new TextWidget(null, "Parameter 2 name bla bla"d)).alignment(Align.Right | Align.VCenter));
34 table2.addChild((new EditLine("edit2", "Some text for parameter 2 blah blah blah"d)).layoutWidth(FILL_PARENT));
35 // row 3
36 table2.addChild((new TextWidget(null, "Param 3"d)).alignment(Align.Right | Align.VCenter));
37 table2.addChild((new EditLine("edit3", "Parameter 3 value"d)).layoutWidth(FILL_PARENT));
38 // row 4
39 table2.addChild((new TextWidget(null, "Param 4"d)).alignment(Align.Right | Align.VCenter));
40 table2.addChild((new EditLine("edit3", "Parameter 4 value shdjksdfh hsjdfas hdjkf hdjsfk ah"d)).layoutWidth(FILL_PARENT));
41 // row 5
42 table2.addChild((new TextWidget(null, "Param 5 - edit text here - blah blah blah"d)).alignment(Align.Right | Align.VCenter));
43 table2.addChild((new EditLine("edit3", "Parameter 5 value"d)).layoutWidth(FILL_PARENT));
44 // row 6
45 table2.addChild((new TextWidget(null, "Param 6 - just to fill content widget"d)).alignment(Align.Right | Align.VCenter));
46 table2.addChild((new EditLine("edit3", "Parameter 5 value"d)).layoutWidth(FILL_PARENT));
47 // row 7
48 table2.addChild((new TextWidget(null, "Param 7 - just to fill content widget"d)).alignment(Align.Right | Align.VCenter));
49 table2.addChild((new EditLine("edit3", "Parameter 5 value"d)).layoutWidth(FILL_PARENT));
50 // row 8
51 table2.addChild((new TextWidget(null, "Param 8 - just to fill content widget"d)).alignment(Align.Right | Align.VCenter));
52 table2.addChild((new EditLine("edit3", "Parameter 5 value"d)).layoutWidth(FILL_PARENT));
53 table2.margins(Rect(10,10,10,10)).layoutWidth(FILL_PARENT);
54 scrollContent.addChild(table2);
55 
56 scrollContent.addChild(new TextWidget(null, "Now - some buttons"d));
57 scrollContent.addChild(new ImageTextButton("btn1", "fileclose", "Close"d));
58 scrollContent.addChild(new ImageTextButton("btn2", "fileopen", "Open"d));
59 scrollContent.addChild(new TextWidget(null, "And checkboxes"d));
60 scrollContent.addChild(new CheckBox("btn1", "CheckBox 1"d));
61 scrollContent.addChild(new CheckBox("btn2", "CheckBox 2"d));
62 
63 scroll.contentWidget = scrollContent;
64 
65 ----
66 
67 Copyright: Vadim Lopatin, 2014
68 License:   Boost License 1.0
69 Authors:   Vadim Lopatin, coolreader.org@gmail.com
70 */
71 module dlangui.widgets.scroll;
72 
73 import dlangui.widgets.widget;
74 import dlangui.widgets.controls;
75 import dlangui.widgets.scrollbar;
76 import std.conv;
77 
78 /** Scroll bar visibility mode. */
79 enum ScrollBarMode {
80     /** always invisible */
81     Invisible,
82     /** always visible */
83     Visible,
84     /** automatically show/hide scrollbar depending on content size */
85     Auto,
86     /** Scrollbar is provided by external control outside this widget */
87     External,
88 }
89 
90 /** 
91     Abstract scrollable widget
92 
93     Provides scroll bars and basic scrolling functionality.
94 
95  */
96 class ScrollWidgetBase :  WidgetGroup, OnScrollHandler {
97     protected ScrollBarMode _vscrollbarMode;
98     protected ScrollBarMode _hscrollbarMode;
99     /// vertical scrollbar control
100     protected ScrollBar _vscrollbar;
101     /// horizontal scrollbar control
102     protected ScrollBar _hscrollbar;
103     /// inner area, excluding additional controls like scrollbars
104     protected Rect _clientRect;
105 
106     protected Rect _fullScrollableArea;
107     protected Rect _visibleScrollableArea;
108 
109     this(string ID = null, ScrollBarMode hscrollbarMode = ScrollBarMode.Visible, ScrollBarMode vscrollbarMode = ScrollBarMode.Visible) {
110         super(ID);
111         _hscrollbarMode = hscrollbarMode;
112         _vscrollbarMode = vscrollbarMode;
113         if (_vscrollbarMode != ScrollBarMode.Invisible) {
114             _vscrollbar = new ScrollBar("vscrollbar", Orientation.Vertical);
115             _vscrollbar.scrollEvent = this;
116             addChild(_vscrollbar);
117         }
118         if (_hscrollbarMode != ScrollBarMode.Invisible) {
119             _hscrollbar = new ScrollBar("hscrollbar", Orientation.Horizontal);
120             _hscrollbar.scrollEvent = this;
121             addChild(_hscrollbar);
122         }
123     }
124 
125     /// vertical scrollbar mode
126     @property ScrollBarMode vscrollbarMode() { return _vscrollbarMode; }
127     @property void vscrollbarMode(ScrollBarMode m) { _vscrollbarMode = m; }
128     /// horizontal scrollbar mode
129     @property ScrollBarMode hscrollbarMode() { return _hscrollbarMode; }
130     @property void hscrollbarMode(ScrollBarMode m) { _hscrollbarMode = m; }
131 
132     /// returns client area rectangle
133     @property Rect clientRect() { return _clientRect; }
134 
135     /// process horizontal scrollbar event
136     bool onHScroll(ScrollEvent event) {
137         return true;
138     }
139 
140     /// process vertical scrollbar event
141     bool onVScroll(ScrollEvent event) {
142         return true;
143     }
144 
145     /// process mouse event; return true if event is processed by widget.
146     override bool onMouseEvent(MouseEvent event) {
147         if (event.action == MouseAction.Wheel) {
148             if (event.flags == MouseFlag.Shift) {
149                 if (_hscrollbar) {
150                     _hscrollbar.sendScrollEvent(event.wheelDelta > 0 ? ScrollAction.LineUp : ScrollAction.LineDown);
151                     return true;
152                 }
153             } else if (event.flags == 0) {
154                 if (_vscrollbar) {
155                     _vscrollbar.sendScrollEvent(event.wheelDelta > 0 ? ScrollAction.LineUp : ScrollAction.LineDown);
156                     return true;
157                 }
158             }
159         }
160         return super.onMouseEvent(event);
161     }
162 
163     /// handle scroll event
164     override bool onScrollEvent(AbstractSlider source, ScrollEvent event) {
165         if (source.orientation == Orientation.Horizontal) {
166             return onHScroll(event);
167         } else {
168             return onVScroll(event);
169         }
170     }
171 
172     /// update scrollbar positions
173     protected void updateScrollBars() {
174         if (_hscrollbar) {
175             updateHScrollBar();
176         }
177         if (_vscrollbar) {
178             updateVScrollBar();
179         }
180     }
181 
182     public @property ScrollBar hscrollbar() { return _hscrollbar; }
183     public @property ScrollBar vscrollbar() { return _vscrollbar; }
184     
185     public @property void hscrollbar(ScrollBar hscroll) { 
186         if (_hscrollbar) {
187             removeChild(_hscrollbar);
188             destroy(_hscrollbar);
189             _hscrollbar = null;
190             _hscrollbarMode = ScrollBarMode.Invisible;
191         }
192         if (hscroll) {
193             _hscrollbar = hscroll;
194             _hscrollbarMode = ScrollBarMode.External;
195         }
196     }
197     
198     public @property void vscrollbar(ScrollBar vscroll) { 
199         if (_vscrollbar) {
200             removeChild(_vscrollbar);
201             destroy(_vscrollbar);
202             _vscrollbar = null;
203             _vscrollbarMode = ScrollBarMode.Invisible;
204         }
205         if (vscroll) {
206             _vscrollbar = vscroll;
207             _vscrollbarMode = ScrollBarMode.External;
208         }
209     }
210 
211     /// update horizontal scrollbar widget position
212     protected void updateHScrollBar() {
213         // default implementation: use _fullScrollableArea, _visibleScrollableArea: override it if necessary
214         _hscrollbar.setRange(0, _fullScrollableArea.width);
215         _hscrollbar.pageSize(_visibleScrollableArea.width);
216         _hscrollbar.position(_visibleScrollableArea.left - _fullScrollableArea.left);
217     }
218 
219     /// update verticat scrollbar widget position
220     protected void updateVScrollBar() {
221         // default implementation: use _fullScrollableArea, _visibleScrollableArea: override it if necessary
222         _vscrollbar.setRange(0, _fullScrollableArea.height);
223         _vscrollbar.pageSize(_visibleScrollableArea.height);
224         _vscrollbar.position(_visibleScrollableArea.top - _fullScrollableArea.top);
225     }
226 
227     protected void drawClient(DrawBuf buf) {
228         // override it
229     }
230 
231     protected void drawExtendedArea(DrawBuf buf) {
232     }
233 
234     /// Draw widget at its position to buffer
235     override void onDraw(DrawBuf buf) {
236         if (visibility != Visibility.Visible)
237             return;
238         super.onDraw(buf);
239         Rect rc = _pos;
240         applyMargins(rc);
241         {
242             auto saver = ClipRectSaver(buf, rc, alpha);
243             DrawableRef bg = backgroundDrawable;
244             if (!bg.isNull) {
245                 bg.drawTo(buf, rc, state);
246             }
247             applyPadding(rc);
248             if (_hscrollbar)
249                 _hscrollbar.onDraw(buf);
250             if (_vscrollbar)
251                 _vscrollbar.onDraw(buf);
252             // apply clipping
253             {
254                 auto saver2 = ClipRectSaver(buf, _clientRect, alpha);
255                 drawClient(buf);
256             }
257             {
258                 // no clipping for drawing of extended area
259                 Rect clipr = rc;
260                 clipr.bottom = _clientRect.bottom;
261                 auto saver3 = ClipRectSaver(buf, clipr, alpha);
262                 drawExtendedArea(buf);
263             }
264         }
265 
266         _needDraw = false;
267     }
268 
269     /// calculate full content size in pixels
270     Point fullContentSize() {
271         // override it
272         Point sz;
273         return sz;
274     }
275 
276     /// Measure widget according to desired width and height constraints. (Step 1 of two phase layout).
277     override void measure(int parentWidth, int parentHeight) { 
278         if (visibility == Visibility.Gone) {
279             return;
280         }
281         Rect m = margins;
282         Rect p = padding;
283         // calc size constraints for children
284         int pwidth = parentWidth;
285         int pheight = parentHeight;
286         if (parentWidth != SIZE_UNSPECIFIED)
287             pwidth -= m.left + m.right + p.left + p.right;
288         if (parentHeight != SIZE_UNSPECIFIED)
289             pheight -= m.top + m.bottom + p.top + p.bottom;
290         if (_hscrollbar && _hscrollbarMode == ScrollBarMode.Visible) {
291             _hscrollbar.measure(pwidth, pheight);
292         }
293         if (_vscrollbar && _vscrollbarMode == ScrollBarMode.Visible) {
294             _vscrollbar.measure(pwidth, pheight);
295         }
296         Point sz = fullContentSize();
297         if (_hscrollbar && _hscrollbarMode == ScrollBarMode.Visible) {
298             sz.y += _hscrollbar.measuredHeight;
299         }
300         if (_vscrollbar && _vscrollbarMode == ScrollBarMode.Visible) {
301             sz.x += _vscrollbar.measuredWidth;
302         }
303         measuredContent(parentWidth, parentHeight, sz.x, sz.y);
304     }
305 
306     /// override to support modification of client rect after change, e.g. apply offset
307     protected void handleClientRectLayout(ref Rect rc) {
308     }
309 
310     /// Set widget rectangle to specified value and layout widget contents. (Step 2 of two phase layout).
311     override void layout(Rect rc) {
312         if (visibility == Visibility.Gone) {
313             return;
314         }
315         _pos = rc;
316         _needLayout = false;
317         applyMargins(rc);
318         applyPadding(rc);
319         Point sz = fullContentSize();
320         bool needHscroll = _hscrollbarMode != ScrollBarMode.External && _hscrollbarMode != ScrollBarMode.Invisible && sz.x > rc.width;
321         bool needVscroll = _vscrollbarMode != ScrollBarMode.External && _vscrollbarMode != ScrollBarMode.Invisible && sz.y > rc.height;
322         if (needVscroll && _vscrollbarMode != ScrollBarMode.Invisible)
323             needHscroll = sz.x > rc.width - _vscrollbar.measuredWidth;
324         if (needHscroll && _hscrollbarMode != ScrollBarMode.Invisible)
325             needVscroll = sz.y > rc.height - _hscrollbar.measuredHeight;
326         if (needVscroll && _vscrollbarMode != ScrollBarMode.Invisible)
327             needHscroll = sz.x > rc.width - _vscrollbar.measuredWidth;
328         needVscroll = needVscroll || (_vscrollbarMode == ScrollBarMode.Visible);
329         needHscroll = needHscroll || (_hscrollbarMode == ScrollBarMode.Visible);
330         // scrollbars
331         Rect vsbrc = rc;
332         vsbrc.left = vsbrc.right - (needVscroll ? _vscrollbar.measuredWidth : 0);
333         vsbrc.bottom = vsbrc.bottom - (needHscroll ? _hscrollbar.measuredHeight : 0);
334         Rect hsbrc = rc;
335         hsbrc.right = hsbrc.right - (needVscroll ? _vscrollbar.measuredWidth : 0);
336         hsbrc.top = hsbrc.bottom - (needHscroll ? _hscrollbar.measuredHeight : 0);
337         if (_vscrollbar && _vscrollbarMode != ScrollBarMode.External) {
338             _vscrollbar.visibility = needVscroll ? Visibility.Visible : Visibility.Gone;
339             _vscrollbar.layout(vsbrc);
340         }
341         if (_hscrollbar && _hscrollbarMode != ScrollBarMode.External) {
342             _hscrollbar.visibility = needHscroll ? Visibility.Visible : Visibility.Gone;
343             _hscrollbar.layout(hsbrc);
344         }
345         // client area
346         _clientRect = rc;
347         handleClientRectLayout(_clientRect);
348         if (needVscroll)
349             _clientRect.right = vsbrc.left;
350         if (needHscroll)
351             _clientRect.bottom = hsbrc.top;
352         updateScrollBars();
353     }
354 
355     void makeRectVisible(Rect rc, bool alignHorizontally = true, bool alignVertically = true) {
356         if (rc.isInsideOf(_visibleScrollableArea))
357             return;
358         Rect oldRect = _visibleScrollableArea;
359         if (alignHorizontally && rc.right > _visibleScrollableArea.right)
360             _visibleScrollableArea.offset(rc.right - _visibleScrollableArea.right, 0);
361         if (alignVertically && rc.bottom > _visibleScrollableArea.bottom)
362             _visibleScrollableArea.offset(0, rc.bottom - _visibleScrollableArea.bottom);
363         if (alignHorizontally && rc.left < _visibleScrollableArea.left)
364             _visibleScrollableArea.offset(rc.left - _visibleScrollableArea.left, 0);
365         if (alignVertically && rc.top < _visibleScrollableArea.top)
366             _visibleScrollableArea.offset(0, rc.top - _visibleScrollableArea.top);
367         if (_visibleScrollableArea != oldRect)
368             requestLayout();
369     }
370 }
371 
372 /**
373     Widget which can show content of widget group with optional scrolling
374 
375     If size of content widget exceeds available space, allows to scroll it.
376  */
377 class ScrollWidget :  ScrollWidgetBase {
378     protected Widget _contentWidget;
379     @property Widget contentWidget() { return _contentWidget; }
380     @property ScrollWidget contentWidget(Widget newContent) { 
381         if (_contentWidget) {
382             removeChild(childIndex(_contentWidget));
383             destroy(_contentWidget);
384         }
385         _contentWidget = newContent;
386         addChild(_contentWidget);
387         requestLayout();
388         return this;
389     }
390     /// empty parameter list constructor - for usage by factory
391     this() {
392         this(null);
393     }
394     /// create with ID parameter
395     this(string ID, ScrollBarMode hscrollbarMode = ScrollBarMode.Visible, ScrollBarMode vscrollbarMode = ScrollBarMode.Visible) {
396         super(ID, hscrollbarMode, vscrollbarMode);
397     }
398 
399     /// calculate full content size in pixels
400     override Point fullContentSize() {
401         // override it
402         Point sz;
403         if (_contentWidget) {
404             _contentWidget.measure(SIZE_UNSPECIFIED, SIZE_UNSPECIFIED);
405             sz.x = _contentWidget.measuredWidth;
406             sz.y = _contentWidget.measuredHeight;
407         }
408         _fullScrollableArea.right = sz.x;
409         _fullScrollableArea.bottom = sz.y;
410         return sz;
411     }
412 
413     /// update scrollbar positions
414     override protected void updateScrollBars() {
415         Point sz = fullContentSize();
416         _visibleScrollableArea.right = _visibleScrollableArea.left + _clientRect.width;
417         _visibleScrollableArea.bottom = _visibleScrollableArea.top + _clientRect.height;
418         // move back if scroll is too big after window resize
419         int extrax = _visibleScrollableArea.right - _fullScrollableArea.right;
420         int extray = _visibleScrollableArea.bottom - _fullScrollableArea.bottom;
421         if (extrax > _visibleScrollableArea.left)
422             extrax = _visibleScrollableArea.left;
423         if (extray > _visibleScrollableArea.top)
424             extray = _visibleScrollableArea.top;
425         if (extrax < 0)
426             extrax = 0;
427         if (extray < 0)
428             extray = 0;
429         _visibleScrollableArea.offset(-extrax, -extray);
430         super.updateScrollBars();
431     }
432 
433     override protected void drawClient(DrawBuf buf) {
434         if (_contentWidget) {
435             Point sz = fullContentSize();
436             Point p = scrollPos;
437             _contentWidget.layout(Rect(_clientRect.left - p.x, _clientRect.top - p.y, _clientRect.left + sz.x - p.x, _clientRect.top + sz.y - p.y));
438             _contentWidget.onDraw(buf);
439         }
440     }
441 
442 
443     @property Point scrollPos() {
444         return Point(_visibleScrollableArea.left - _fullScrollableArea.left, _visibleScrollableArea.top - _fullScrollableArea.top);
445     }
446 
447     protected void scrollTo(int x, int y) {
448         if (x > _fullScrollableArea.right - _visibleScrollableArea.width)
449             x = _fullScrollableArea.right - _visibleScrollableArea.width;
450         if (y > _fullScrollableArea.bottom - _visibleScrollableArea.height)
451             y = _fullScrollableArea.bottom - _visibleScrollableArea.height;
452         if (x < 0)
453             x = 0;
454         if (y < 0)
455             y = 0;
456         _visibleScrollableArea.left = x;
457         _visibleScrollableArea.top = y;
458         updateScrollBars();
459         invalidate();
460     }
461 
462     /// process horizontal scrollbar event
463     override bool onHScroll(ScrollEvent event) {
464         if (event.action == ScrollAction.SliderMoved || event.action == ScrollAction.SliderReleased) {
465             scrollTo(event.position, scrollPos.y);
466         } else if (event.action == ScrollAction.PageUp) {
467             scrollTo(scrollPos.x - _clientRect.width * 3 / 4, scrollPos.y);
468         } else if (event.action == ScrollAction.PageDown) {
469             scrollTo(scrollPos.x + _clientRect.width * 3 / 4, scrollPos.y);
470         } else if (event.action == ScrollAction.LineUp) {
471             scrollTo(scrollPos.x - _clientRect.width / 10, scrollPos.y);
472         } else if (event.action == ScrollAction.LineDown) {
473             scrollTo(scrollPos.x + _clientRect.width / 10, scrollPos.y);
474         }
475         return true;
476     }
477 
478     /// process vertical scrollbar event
479     override bool onVScroll(ScrollEvent event) {
480         if (event.action == ScrollAction.SliderMoved || event.action == ScrollAction.SliderReleased) {
481             scrollTo(scrollPos.x, event.position);
482         } else if (event.action == ScrollAction.PageUp) {
483             scrollTo(scrollPos.x, scrollPos.y - _clientRect.height * 3 / 4);
484         } else if (event.action == ScrollAction.PageDown) {
485             scrollTo(scrollPos.x, scrollPos.y + _clientRect.height * 3 / 4);
486         } else if (event.action == ScrollAction.LineUp) {
487             scrollTo(scrollPos.x, scrollPos.y - _clientRect.height / 10);
488         } else if (event.action == ScrollAction.LineDown) {
489             scrollTo(scrollPos.x, scrollPos.y + _clientRect.height / 10);        }
490         return true;
491     }
492 
493     void makeWidgetVisible(Widget widget, bool alignHorizontally = true, bool alignVertically = true) {
494         if (!widget || !widget.visibility == Visibility.Gone)
495             return;
496         if (!_contentWidget || !_contentWidget.isChild(widget))
497             return;
498         Rect wpos = widget.pos;
499         Rect cpos = _contentWidget.pos;
500         wpos.offset(-cpos.left, -cpos.top);
501         makeRectVisible(wpos, alignHorizontally, alignVertically);
502     }
503 }