0338d82c55
Rewrite the way scrolling for ScrollAreas is handled: Store content position instead of scrollbar positions to keep position on resize and promote moving the content instead of the contents to as primary API. Let the mousewheel scroll by fixed content offset instead of scrollbar offset to make it actually usable (especially with low scrolling distance).
254 lines
6.8 KiB
Text
254 lines
6.8 KiB
Text
gui.widgets.ScrollArea = {
|
|
new: func(parent, style, cfg)
|
|
{
|
|
var cfg = Config.new(cfg);
|
|
var m = gui.Widget.new(gui.widgets.ScrollArea);
|
|
m._focus_policy = m.NoFocus;
|
|
m._content_pos = [0, 0];
|
|
m._scroller_pos = [0, 0];
|
|
m._max_scroll = [0, 0];
|
|
m._layout = nil;
|
|
|
|
if( style != nil )
|
|
m._setView( style.createWidget(parent, "scroll-area", cfg) );
|
|
|
|
m.setMinimumSize([32, 32]);
|
|
|
|
return m;
|
|
},
|
|
setLayout: func(l)
|
|
{
|
|
me._layout = l;
|
|
l.setParent(me);
|
|
return me.update();
|
|
},
|
|
getContent: func()
|
|
{
|
|
return me._view.content;
|
|
},
|
|
# Set the background color for the content area.
|
|
#
|
|
# @param color Vector of 3 or 4 values in [0, 1]
|
|
setColorBackground: func
|
|
{
|
|
if( size(arg) == 1 )
|
|
var arg = arg[0];
|
|
me._view.setColorBackground(arg);
|
|
},
|
|
# Reset the size of the content area, e.g. on window resize.
|
|
#
|
|
# @param sz Vector of [x,y] values.
|
|
setSize: func
|
|
{
|
|
if( size(arg) == 1 )
|
|
var arg = arg[0];
|
|
var (x,y) = arg;
|
|
me._size = [x,y];
|
|
me.update();
|
|
|
|
if( me._layout != nil )
|
|
me._layout.setGeometry(me._layout.geometry());
|
|
},
|
|
# Move contents to the coordinates x,y (or as far as possible)
|
|
#
|
|
# @param x The x coordinate (positive is right)
|
|
# @param y The y coordinate (positive is down)
|
|
scrollTo: func(x, y)
|
|
{
|
|
me._content_pos[0] = x;
|
|
me._content_pos[1] = y;
|
|
|
|
me.update();
|
|
},
|
|
# Move the scrollable area to the top-most position
|
|
scrollToTop: func me.scrollTo( me._content_pos[0], 0 ),
|
|
# Move the scrollable area to the bottom-most position
|
|
scrollToBottom: func me.scrollTo( me._content_pos[0], me._max_scroll[1] ),
|
|
# Move the scrollable area to the left-most position
|
|
scrollToLeft: func me.scrollTo( 0, me._content_pos[1] ),
|
|
# Move the scrollable area to the right-most position
|
|
scrollToRight: func me.scrollTo( me._max_scroll[0], me._content_pos[1] ),
|
|
# Move content by given delta
|
|
scrollBy: func(x, y)
|
|
{
|
|
return me.scrollTo( me._content_pos[0] + x,
|
|
me._content_pos[1] + y );
|
|
},
|
|
# Set horizontal scrollbar position
|
|
horizScrollBarTo: func(x)
|
|
{
|
|
if( me._scroller_delta[0] < 1 )
|
|
return me;
|
|
|
|
me.scrollTo( me._max_scroll[0] * (x / me._scroller_delta[0]),
|
|
me._content_pos[1] );
|
|
},
|
|
# Set vertical scrollbar position
|
|
vertScrollBarTo: func(y)
|
|
{
|
|
if( me._scroller_delta[1] < 1 )
|
|
return me;
|
|
|
|
me.scrollTo( me._content_pos[0],
|
|
me._max_scroll[1] * (y / me._scroller_delta[1]) );
|
|
},
|
|
# Move horizontal scrollbar by given offset
|
|
horizScrollBarBy: func(dx)
|
|
{
|
|
me.horizScrollBarTo(me._scroller_pos[0] + dx);
|
|
},
|
|
# Move vertical scrollbar by given offset
|
|
vertScrollBarBy: func(dy)
|
|
{
|
|
me.vertScrollBarTo(me._scroller_pos[1] + dy);
|
|
},
|
|
# Update scroll bar and content area.
|
|
#
|
|
# Needs to be called when the size of the content changes.
|
|
update: func(bb=nil)
|
|
{
|
|
if (bb == nil) bb = me._updateBB();
|
|
if (bb == nil) return me;
|
|
|
|
var offset = [ me._content_offset[0] - me._content_pos[0],
|
|
me._content_offset[1] - me._content_pos[1] ];
|
|
me.getContent().setTranslation(offset);
|
|
|
|
me._view.update(me);
|
|
me.getContent().update();
|
|
|
|
return me;
|
|
},
|
|
# protected:
|
|
_setView: func(view)
|
|
{
|
|
view.vert.addEventListener("mousedown", func(e) me._dragStart(e));
|
|
view.horiz.addEventListener("mousedown", func(e) me._dragStart(e));
|
|
view._root.addEventListener("mousedown", func(e)
|
|
{
|
|
me._drag_offsetX = me._content_pos[0] + e.clientX;
|
|
me._drag_offsetY = me._content_pos[1] + e.clientY;
|
|
});
|
|
|
|
view.vert.addEventListener
|
|
(
|
|
"drag",
|
|
func(e)
|
|
{
|
|
if( !me._enabled )
|
|
return;
|
|
|
|
me.vertScrollBarTo(me._drag_offsetY + e.clientY);
|
|
e.stopPropagation();
|
|
}
|
|
);
|
|
view.horiz.addEventListener
|
|
(
|
|
"drag",
|
|
func(e)
|
|
{
|
|
if( !me._enabled )
|
|
return;
|
|
|
|
me.horizScrollBarTo(me._drag_offsetX + e.clientX);
|
|
e.stopPropagation();
|
|
}
|
|
);
|
|
|
|
view._root.addEventListener
|
|
(
|
|
"drag",
|
|
func(e)
|
|
{
|
|
if( !me._enabled )
|
|
return;
|
|
|
|
me.scrollTo( me._drag_offsetX - e.clientX,
|
|
me._drag_offsetY - e.clientY );
|
|
e.stopPropagation();
|
|
}
|
|
);
|
|
view._root.addEventListener
|
|
(
|
|
"wheel",
|
|
func(e)
|
|
{
|
|
if( !me._enabled )
|
|
return;
|
|
|
|
me.scrollBy(0, 30 * -e.deltaY); # TODO make step size configurable
|
|
e.stopPropagation();
|
|
}
|
|
);
|
|
|
|
call(gui.Widget._setView, [view], me);
|
|
},
|
|
_dragStart: func(e)
|
|
{
|
|
me._drag_offsetX = me._scroller_pos[0] - e.clientX;
|
|
me._drag_offsetY = me._scroller_pos[1] - e.clientY;
|
|
e.stopPropagation();
|
|
},
|
|
_updateBB: func()
|
|
{
|
|
# TODO only update on content resize
|
|
if( me._layout == nil )
|
|
{
|
|
var bb = me.getContent().getTightBoundingBox();
|
|
|
|
if( bb[2] < bb[0] or bb[3] < bb[1] )
|
|
return nil;
|
|
var w = bb[2] - bb[0];
|
|
var h = bb[3] - bb[1];
|
|
|
|
var cur_offset = me.getContent().getTranslation();
|
|
me._content_offset = [cur_offset[0] - bb[0], cur_offset[1] - bb[1]];
|
|
}
|
|
else
|
|
{
|
|
var min_size = me._layout.minimumSize();
|
|
var max_size = me._layout.maximumSize();
|
|
var size_hint = me._layout.sizeHint();
|
|
var w = math.min(max_size[0], math.max(size_hint[0], me._size[0]));
|
|
var h = math.max(
|
|
math.min(max_size[1], math.max(size_hint[1], me._size[1])),
|
|
me._layout.heightForWidth(w)
|
|
);
|
|
|
|
me._layout.setGeometry([0, 0, w, h]);
|
|
|
|
# Layout always has the origin at (0, 0)
|
|
me._content_offset = [0, 0];
|
|
}
|
|
|
|
me._max_scroll[0] = math.max(0, w - me._size[0]);
|
|
me._max_scroll[1] = math.max(0, h - me._size[1]);
|
|
me._content_size = [w, h];
|
|
|
|
# keep position within limit and only integer (to prevent artifacts on text,
|
|
# lines, etc. not alligned with pixel grid)
|
|
me._content_pos[0] =
|
|
math.max(0, math.min( math.round(me._content_pos[0]), me._max_scroll[0]));
|
|
me._content_pos[1] =
|
|
math.max(0, math.min( math.round(me._content_pos[1]), me._max_scroll[1]));
|
|
|
|
me._scroller_size = [0, 0]; # scroller size
|
|
me._scroller_offset = [0, 0]; # scroller minimum pos (eg. add offset for
|
|
# scrolling with buttons)
|
|
me._scroller_delta = [0, 0]; # scroller max travel distance
|
|
|
|
# update scroller size/offset/max delta
|
|
me._view._updateScrollMetrics(me, 0);
|
|
me._view._updateScrollMetrics(me, 1);
|
|
|
|
# update current scrollbar positions
|
|
me._scroller_pos[0] =
|
|
me._max_scroll[0] > 0
|
|
? (me._content_pos[0] / me._max_scroll[0]) * me._scroller_delta[0]
|
|
: 0;
|
|
me._scroller_pos[1] =
|
|
me._max_scroll[1] > 0
|
|
? (me._content_pos[1] / me._max_scroll[1]) * me._scroller_delta[1]
|
|
: 0;
|
|
}
|
|
};
|