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);
    return me;
  },
  # 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];
    return me.update();
  },
  # 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;

    return 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)
  {
    call(gui.Widget._setView, [view], me);

    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();
      }
    );
  },
  _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;
  }
};