1
0
Fork 0
fgdata/Nasal/canvas/gui/widgets/ScrollArea.nas
Thomas Geymayer 0338d82c55 canvas.gui.ScrollArea: fix (wheel) scroll behavior.
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).
2014-06-26 00:50:46 +02:00

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;
}
};