haxeui/haxeui-core

View on GitHub
haxe/ui/containers/ScrollView.hx

Summary

Maintainability
Test Coverage
package haxe.ui.containers;

import haxe.ui.actions.ActionType;
import haxe.ui.behaviours.Behaviour;
import haxe.ui.behaviours.DataBehaviour;
import haxe.ui.behaviours.DefaultBehaviour;
import haxe.ui.components.HorizontalScroll;
import haxe.ui.components.Label;
import haxe.ui.components.Scroll;
import haxe.ui.components.VerticalScroll;
import haxe.ui.constants.MouseButton;
import haxe.ui.constants.Priority;
import haxe.ui.constants.ScrollMode;
import haxe.ui.constants.ScrollPolicy;
import haxe.ui.core.Component;
import haxe.ui.core.CompositeBuilder;
import haxe.ui.core.IScroller;
import haxe.ui.core.InteractiveComponent;
import haxe.ui.core.Screen;
import haxe.ui.events.ActionEvent;
import haxe.ui.events.MouseEvent;
import haxe.ui.events.ScrollEvent;
import haxe.ui.events.UIEvent;
import haxe.ui.geom.Point;
import haxe.ui.geom.Rectangle;
import haxe.ui.geom.Size;
import haxe.ui.layouts.LayoutFactory;
import haxe.ui.layouts.ScrollViewLayout;
import haxe.ui.styles.Style;
import haxe.ui.util.Timer;
import haxe.ui.util.Variant;
import haxe.ui.validation.InvalidationFlags;

@:composite(ScrollViewEvents, ScrollViewBuilder, ScrollViewLayout)
class ScrollView extends InteractiveComponent implements IScroller {
    //***********************************************************************************************************
    // Public API
    //***********************************************************************************************************
    @:clonable @:behaviour(Virtual)                                 public var virtual:Bool;
    @:clonable @:behaviour(ContentLayoutName, "vertical")           public var contentLayoutName:String;
    @:clonable @:behaviour(ContentWidth)                            public var contentWidth:Null<Float>;
    @:clonable @:behaviour(PercentContentWidth)                     public var percentContentWidth:Null<Float>;
    @:clonable @:behaviour(ContentHeight)                           public var contentHeight:Null<Float>;
    @:clonable @:behaviour(PercentContentHeight)                    public var percentContentHeight:Null<Float>;
    @:clonable @:behaviour(HScrollPos)                              public var hscrollPos:Float;
    @:clonable @:behaviour(HScrollMax)                              public var hscrollMax:Float;
    @:clonable @:behaviour(HScrollPageSize)                         public var hscrollPageSize:Float;
    @:clonable @:behaviour(HScrollThumbSize)                        public var hscrollThumbSize:Null<Float>;
    @:clonable @:behaviour(VScrollPos)                              public var vscrollPos:Float;
    @:clonable @:behaviour(VScrollMax)                              public var vscrollMax:Float;
    @:clonable @:behaviour(VScrollPageSize)                         public var vscrollPageSize:Float;
    @:clonable @:behaviour(VScrollThumbSize)                        public var vscrollThumbSize:Null<Float>;
    @:clonable @:behaviour(ThumbSize)                               public var thumbSize:Null<Float>;
    @:clonable @:behaviour(DefaultBehaviour, MouseButton.LEFT)      public var scrollMouseButton:MouseButton;
    @:clonable @:behaviour(ScrollModeBehaviour, ScrollMode.DEFAULT) public var scrollMode:ScrollMode;
    @:clonable @:behaviour(ScrollPolicyBehaviour)                   public var scrollPolicy:ScrollPolicy;
    @:clonable @:behaviour(HScrollPolicyBehaviour)                  public var horizontalScrollPolicy:ScrollPolicy;
    @:clonable @:behaviour(VScrollPolicyBehaviour)                  public var verticalScrollPolicy:ScrollPolicy;
    @:clonable @:behaviour(GetContents)                             public var contents:Component;
    @:clonable @:behaviour(DefaultBehaviour)                        public var autoHideScrolls:Bool;
    @:clonable @:behaviour(DefaultBehaviour, true)                  public var allowAutoScroll:Bool;
    @:clonable @:behaviour(IsScrollableHorizontallyBehaviour)       public var isScrollableHorizontally:Bool;
    @:clonable @:behaviour(IsScrollableVerticallyBehaviour)         public var isScrollableVertically:Bool;
    @:clonable @:behaviour(IsScrollableBehaviour)                   public var isScrollable:Bool;
    @:clonable @:behaviour(EmptyContentsComponent)                  public var emptyContentsComponent:Component;
    @:clonable @:behaviour(EmptyContentsText)                       public var emptyContentsText:String;
    
    @:call(EnsureVisible)                                           public function ensureVisible(component:Component):Void;
    @:call(FindHorizontalScrollbar)                                 public function findHorizontalScrollbar():Component;
    @:call(FindVerticalScrollbar)                                   public function findVerticalScrollbar():Component;

    @:event(ScrollEvent.SCROLL)                                     public var onScroll:ScrollEvent->Void;
    
    //***********************************************************************************************************
    // Validation
    //***********************************************************************************************************
    private override function validateComponentInternal(nextFrame:Bool = true) { // TODO: can this be moved to CompositeBuilder? Like validateComponentLayout?
        if (native == true) { // TODO:  teeeeeemp! This should _absolutely_ be part of CompositeBuilder as native components try to call it and things like checkScrolls dont make sense
            super.validateComponentInternal(nextFrame);
            return;
        }
        var scrollInvalid = isComponentInvalid(InvalidationFlags.SCROLL);
        var layoutInvalid = isComponentInvalid(InvalidationFlags.LAYOUT);

        super.validateComponentInternal(nextFrame);

        if (scrollInvalid || layoutInvalid) {
            cast(_compositeBuilder, ScrollViewBuilder).checkScrolls(); // TODO: would be nice to not have this
            cast(_compositeBuilder, ScrollViewBuilder).updateScrollRect(); // TODO: or this
        }
    }
}

//***********************************************************************************************************
// Behaviours
//***********************************************************************************************************
@:dox(hide) @:noCompletion
@:access(haxe.ui.core.Component)
private class EnsureVisible extends DefaultBehaviour {
    public override function call(param:Any = null):Variant {
        var scrollview:ScrollView = cast(_component, ScrollView);
        if (scrollview.allowAutoScroll == false) {
            return null;
        }
        
        var c:Component = cast(param, Component);
        if (c == scrollview) {
            return null;
        }

        if (!scrollview.isReady) {
            scrollview.registerEvent(UIEvent.READY, function f(_) {
                scrollview.ensureVisible(c);
            });
            return null;
        }

        var hscroll:HorizontalScroll = scrollview.findComponent(HorizontalScroll, false);
        var hscrollPos:Float = 0;
        if (hscroll != null) {
            hscrollPos = hscroll.pos;
        }
        
        var vscroll:VerticalScroll = scrollview.findComponent(VerticalScroll, false);
        var vscrollPos:Float = 0;
        if (vscroll != null) {
            vscrollPos = vscroll.pos;
        }
        
        var componentScreenRect = new Rectangle(c.screenLeft, c.screenTop, c.width, c.height);
        var componentRect = new Rectangle(c.screenLeft + hscrollPos, c.screenTop + vscrollPos, c.width, c.height);
        var scrollRect = new Rectangle(scrollview.screenLeft, scrollview.screenTop, scrollview.width, scrollview.height);
        
        var scrollRectFixed = scrollRect.copy();
        var usableSize = scrollview.layout.usableSize;
        scrollRectFixed.width = usableSize.width;
        scrollRectFixed.height = usableSize.height;

        if (scrollRectFixed.containsRect(componentScreenRect)) { // fully contains child rect, do nothing
            return null;
        }
        
        var newHScrollPos = hscrollPos;
        var newVScrollPos = vscrollPos;
        
        var fixedRight = componentRect.right - scrollRect.left;
        var fixedLeft = componentRect.left - scrollRect.left;
        var fixedBottom = componentRect.bottom - scrollRect.top;
        var fixedTop = componentRect.top - scrollRect.top;
        var offsetLeft = 1;// contentsRect.left - scrollRect.left;
        var offsetTop = 1;// contentsRect.top - scrollRect.top;
        
        if (scrollRectFixed.containsPoint(componentScreenRect.right, componentScreenRect.top) == false) {
            newHScrollPos = fixedRight - (usableSize.width) + (calcOffset(c, "right") - offsetLeft);
        } else if (scrollRectFixed.containsPoint(componentScreenRect.left, componentScreenRect.top) == false) {
            newHScrollPos = fixedLeft - (calcOffset(c, "left") + offsetLeft);
        }
        
        if (scrollRectFixed.containsPoint(componentScreenRect.left, componentScreenRect.bottom) == false) {
            newVScrollPos = fixedBottom - (usableSize.height) + (calcOffset(c, "bottom") - offsetTop);
        } else if (scrollRectFixed.containsPoint(componentScreenRect.left, componentScreenRect.top) == false) {
            newVScrollPos = fixedTop - (calcOffset(c, "top") + offsetTop);
        }
        
        if (hscroll != null) {
            hscroll.pos = newHScrollPos;
        }
        if (vscroll != null) {
            vscroll.pos = newVScrollPos;
        }
        
        return null;
    }
    
    private function calcOffset(c:Component, which:String) {
        var p:Float = 0;
        var r = c.parentComponent;
        while (r != null) {
            if (r.style != null) {
                switch (which) {
                    case "left":
                        if (r.paddingLeft != null) {
                            p += r.paddingLeft;
                        }
                    case "right":
                        if (r.paddingRight != null) {
                            p += r.paddingRight;
                        }
                    case "top":
                        if (r.paddingTop != null) {
                            p += r.paddingTop;
                        }
                    case "bottom":
                        if (r.paddingBottom != null) {
                            p += r.paddingBottom;
                        }
                }
            }
            r = r.parentComponent;
            if (r == _component) {
                break;
            }
        }
        return p;
    }
}

@:dox(hide) @:noCompletion
@:access(haxe.ui.core.Component)
private class FindHorizontalScrollbar extends DefaultBehaviour {
    public override function call(param:Any = null):Variant {
        var scrollview:ScrollView = cast(_component, ScrollView);
        return scrollview.findComponent(HorizontalScroll, false);
    }
}

@:dox(hide) @:noCompletion
@:access(haxe.ui.core.Component)
private class FindVerticalScrollbar extends DefaultBehaviour {
    public override function call(param:Any = null):Variant {
        var scrollview:ScrollView = cast(_component, ScrollView);
        return scrollview.findComponent(VerticalScroll, false);
    }
}

@:dox(hide) @:noCompletion
@:access(haxe.ui.core.Component)
private class Virtual extends DefaultBehaviour {
    public override function set(value:Variant) {
        super.set(value);
        if (_component._compositeBuilder != null) {
            cast(_component._compositeBuilder, ScrollViewBuilder).onVirtualChanged();
        }
    }
}

@:dox(hide) @:noCompletion
@:access(haxe.ui.core.Component)
@:access(haxe.ui.containers.ScrollViewBuilder)
private class ContentLayoutName extends DefaultBehaviour {
    public override function set(value:Variant) {
        super.set(value);
        var builder = cast(_component._compositeBuilder, ScrollViewBuilder);
        if (builder != null && builder._contentsLayoutName != value) {
            builder._contentsLayoutName = value;
            builder._contents.layout = LayoutFactory.createFromName(value);
        }
    }
}

@:dox(hide) @:noCompletion
private class ContentWidth extends Behaviour {
    public override function get():Variant {
        var contents:Component = _component.findComponent("scrollview-contents", false, "css");
        if (contents == null) {
            return null;
        }
        return contents.width;
    }

    public override function set(value:Variant) {
        var contents:Component = _component.findComponent("scrollview-contents", false, "css");
        if (contents != null) {
            contents.percentWidth = null;
            contents.width = value;
        }
    }
}

@:dox(hide) @:noCompletion
@:access(haxe.ui.core.Component)
private class PercentContentWidth extends Behaviour {
    public override function get():Variant {
        var contents:Component = _component.findComponent("scrollview-contents", false, "css");
        if (contents == null) {
            return null;
        }
        return contents.percentWidth;
    }

    public override function set(value:Variant) {
        var contents:Component = _component.findComponent("scrollview-contents", false, "css");
        if (contents != null) {
            contents.componentWidth = null;
            contents.percentWidth = value;
        }
    }
}

@:dox(hide) @:noCompletion
private class ContentHeight extends Behaviour {
    public override function get():Variant {
        var contents:Component = _component.findComponent("scrollview-contents", false, "css");
        if (contents == null) {
            return null;
        }
        return contents.height;
    }

    public override function set(value:Variant) {
        var contents:Component = _component.findComponent("scrollview-contents", false, "css");
        if (contents != null) {
            contents.percentHeight = null;
            contents.height = value;
        }
    }
}

@:dox(hide) @:noCompletion
@:access(haxe.ui.core.Component)
private class PercentContentHeight extends Behaviour {
    public override function get():Variant {
        var contents:Component = _component.findComponent("scrollview-contents", false, "css");
        if (contents == null) {
            return null;
        }
        return contents.percentHeight;
    }

    public override function set(value:Variant) {
        var contents:Component = _component.findComponent("scrollview-contents", false, "css");
        if (contents != null) {
            contents.componentHeight = null;
            contents.percentHeight = value;
        }
    }
}

@:dox(hide) @:noCompletion
@:access(haxe.ui.core.Component)
private class HScrollPos extends DataBehaviour {
    private var _scrollview:ScrollView;

    public function new(scrollview:ScrollView) {
        super(scrollview);
        _scrollview = scrollview;
    }

    public override function get():Variant {
        var hscroll = _scrollview.findComponent(HorizontalScroll, false);
        if (hscroll == null) {
            return 0;
        }
        return hscroll.pos;
    }

    public override function validateData() { // TODO: feels a bit ugly!
        var hscroll = _scrollview.findComponent(HorizontalScroll, false);
        if (_scrollview.virtual == true) {
            if (hscroll == null) {
                hscroll = cast(_scrollview._compositeBuilder, ScrollViewBuilder).createHScroll();
            }
            if (hscroll != null) {
                hscroll.pos = _value;
            }

        } else if (hscroll != null) {
            hscroll.pos = _value;
        }
    }
}

@:dox(hide) @:noCompletion
@:access(haxe.ui.core.Component)
private class VScrollPos extends DataBehaviour {
    private var _scrollview:ScrollView;

    public function new(scrollview:ScrollView) {
        super(scrollview);
        _scrollview = scrollview;
    }

    public override function get():Variant {
        var vscroll = _scrollview.findComponent(VerticalScroll, false);
        if (vscroll == null) {
            return 0;
        }
        return vscroll.pos;
    }

    public override function validateData() { // TODO: feels a bit ugly!
        var vscroll = _scrollview.findComponent(VerticalScroll, false);
        if (_scrollview.virtual == true) {
            if (vscroll == null) {
                vscroll = cast(_scrollview._compositeBuilder, ScrollViewBuilder).createVScroll();
            }
            if (vscroll != null) {
                vscroll.pos = _value;
            }

        } else if (vscroll != null) {
            vscroll.pos = _value;
        }
    }
}

@:dox(hide) @:noCompletion
@:access(haxe.ui.core.Component)
private class HScrollMax extends DataBehaviour {
    private var _scrollview:ScrollView;

    public function new(scrollview:ScrollView) {
        super(scrollview);
        _scrollview = scrollview;
    }

    public override function get():Variant {
        var hscroll = _scrollview.findComponent(HorizontalScroll, false);
        if (hscroll == null) {
            return 0;
        }
        return hscroll.max;
    }

    public override function validateData() { // TODO: feels a bit ugly!
        if (_scrollview.virtual == true) {
            var hscroll = _scrollview.findComponent(HorizontalScroll, false);
            if (_value > 0) {
                if (hscroll == null) {
                    hscroll = cast(_scrollview._compositeBuilder, ScrollViewBuilder).createHScroll();
                }
            } else {
                if (hscroll != null) {
                    cast(_scrollview._compositeBuilder, ScrollViewBuilder).destroyHScroll();
                }
            }
            if (hscroll != null) {
                hscroll.max = _value;
            }
        }
    }
}

@:dox(hide) @:noCompletion
@:access(haxe.ui.core.Component)
private class VScrollMax extends DataBehaviour {
    private var _scrollview:ScrollView;

    public function new(scrollview:ScrollView) {
        super(scrollview);
        _scrollview = scrollview;
    }

    public override function get():Variant {
        var vscroll = _scrollview.findComponent(VerticalScroll, false);
        if (vscroll == null) {
            return 0;
        }
        return vscroll.max;
    }

    public override function validateData() { // TODO: feels a bit ugly!
        if (_scrollview.virtual == true) {
            var vscroll = _scrollview.findComponent(VerticalScroll, false);
            if (_value > 0) {
                if (vscroll == null) {
                    vscroll = cast(_scrollview._compositeBuilder, ScrollViewBuilder).createVScroll();
                }
            } else {
                if (vscroll != null) {
                    cast(_scrollview._compositeBuilder, ScrollViewBuilder).destroyVScroll();
                }
            }
            if (vscroll != null) {
                vscroll.max = _value;
            }
        }
    }
}

@:dox(hide) @:noCompletion
@:access(haxe.ui.core.Component)
private class HScrollPageSize extends DataBehaviour {
    private var _scrollview:ScrollView;

    public function new(scrollview:ScrollView) {
        super(scrollview);
        _scrollview = scrollview;
    }

    public override function validateData() { // TODO: feels a bit ugly!
        if (_scrollview.virtual == true) {
            var hscroll = _scrollview.findComponent(HorizontalScroll, false);
            if (hscroll == null) {
                hscroll = cast(_scrollview._compositeBuilder, ScrollViewBuilder).createHScroll();
            }
            if (hscroll != null) {
                hscroll.pageSize = _value;
            }
        }
    }
}

@:dox(hide) @:noCompletion
@:access(haxe.ui.core.Component)
private class HScrollThumbSize extends DataBehaviour {
    private var _scrollview:ScrollView;

    public function new(scrollview:ScrollView) {
        super(scrollview);
        _scrollview = scrollview;
    }

    public override function validateData() { // TODO: feels a bit ugly!
        if (_scrollview.virtual == true) {
            var hscroll = _scrollview.findComponent(HorizontalScroll, false);
            if (hscroll == null) {
                hscroll = cast(_scrollview._compositeBuilder, ScrollViewBuilder).createHScroll();
            }
            if (hscroll != null) {
                hscroll.thumbSize = _value;
            }
        }
    }
}

@:dox(hide) @:noCompletion
@:access(haxe.ui.core.Component)
private class VScrollPageSize extends DataBehaviour {
    private var _scrollview:ScrollView;

    public function new(scrollview:ScrollView) {
        super(scrollview);
        _scrollview = scrollview;
    }

    public override function validateData() { // TODO: feels a bit ugly!
        if (_scrollview.virtual == true) {
            var vscroll = _scrollview.findComponent(VerticalScroll, false);
            if (vscroll == null) {
                vscroll = cast(_scrollview._compositeBuilder, ScrollViewBuilder).createVScroll();
            }
            if (vscroll != null) {
                vscroll.pageSize = _value;
            }
        }
    }
}

@:dox(hide) @:noCompletion
@:access(haxe.ui.core.Component)
private class VScrollThumbSize extends DataBehaviour {
    private var _scrollview:ScrollView;

    public function new(scrollview:ScrollView) {
        super(scrollview);
        _scrollview = scrollview;
    }

    public override function validateData() { // TODO: feels a bit ugly!
        if (_scrollview.virtual == true) {
            var vscroll = _scrollview.findComponent(VerticalScroll, false);
            if (vscroll == null) {
                vscroll = cast(_scrollview._compositeBuilder, ScrollViewBuilder).createVScroll();
            }
            if (vscroll != null) {
                vscroll.thumbSize = _value;
            }
        }
    }
}

@:dox(hide) @:noCompletion
@:access(haxe.ui.core.Component)
private class ThumbSize extends DataBehaviour {
    private var _scrollview:ScrollView;

    public function new(scrollview:ScrollView) {
        super(scrollview);
        _scrollview = scrollview;
    }

    public override function validateData() { 
        _scrollview.hscrollThumbSize = _value;
        _scrollview.vscrollThumbSize = _value;
    }
}

@:dox(hide) @:noCompletion
@:access(haxe.ui.core.Component)
private class ScrollModeBehaviour extends DataBehaviour {
    public override function set(value:Variant) {
        if (value == ScrollMode.HYBRID) {
            _component.isHybridScroller = true;
        } else if (value == ScrollMode.NATIVE) {
            _component.isNativeScroller = true;
        }
        super.set(value);
    }

    public override function validateData() {
        _component.registerInternalEvents(true);
    }
}

@:dox(hide) @:noCompletion
@:access(haxe.ui.core.Component)
private class IsScrollableHorizontallyBehaviour extends DefaultBehaviour {
    private var _scrollview:ScrollView;

    public function new(scrollview:ScrollView) {
        super(scrollview);
        _scrollview = scrollview;
    }

    public override function get():Variant {
        var hscroll = _scrollview.findComponent("scrollview-hscroll", HorizontalScroll);
        if (hscroll == null) { // seems we never need anything more (like checking the values of the scroll - if the scrollview cant scroll scrollbar is destroyed, and therefore null)
            return false;
        }
        return true;
    }
}

@:dox(hide) @:noCompletion
@:access(haxe.ui.core.Component)
private class IsScrollableVerticallyBehaviour extends DefaultBehaviour {
    private var _scrollview:ScrollView;

    public function new(scrollview:ScrollView) {
        super(scrollview);
        _scrollview = scrollview;
    }

    public override function get():Variant {
        var vscroll = _scrollview.findComponent("scrollview-vscroll", VerticalScroll);
        if (vscroll == null) { // seems we never need anything more (like checking the values of the scroll - if the scrollview cant scroll scrollbar is destroyed, and therefore null)
            return false;
        }
        return true;
    }
}

@:dox(hide) @:noCompletion
@:access(haxe.ui.core.Component)
private class IsScrollableBehaviour extends DefaultBehaviour {
    private var _scrollview:ScrollView;

    public function new(scrollview:ScrollView) {
        super(scrollview);
        _scrollview = scrollview;
    }

    public override function get():Variant {
        if (_scrollview.isScrollableVertically) { // more like to be scrolling vertically, so lets make that check first
            return true;
        }
        if (_scrollview.isScrollableHorizontally) {
            return true;
        }
        return false;
    }
}

@:dox(hide) @:noCompletion
@:access(haxe.ui.core.Component)
private class ScrollPolicyBehaviour extends DataBehaviour {
    private var _scrollview:ScrollView;

    public function new(scrollview:ScrollView) {
        super(scrollview);
        _scrollview = scrollview;
    }

    public override function validateData() {
        _scrollview.horizontalScrollPolicy = _value;
        _scrollview.verticalScrollPolicy = _value;
    }
}

@:dox(hide) @:noCompletion
@:access(haxe.ui.core.Component)
private class HScrollPolicyBehaviour extends DataBehaviour {
}

@:dox(hide) @:noCompletion
@:access(haxe.ui.core.Component)
private class VScrollPolicyBehaviour extends DataBehaviour {
}

@:dox(hide) @:noCompletion
private class GetContents extends DefaultBehaviour {
    public override function get():Variant {
        var contents:Component = _component.findComponent("scrollview-contents", false, "css");
        return contents;
    }
}

@:dox(hide) @:noCompletion
@:access(haxe.ui.core.Component)
@:access(haxe.ui.containers.ScrollViewBuilder)
private class EmptyContentsComponent extends DefaultBehaviour {
    public override function set(value:Variant):Void {
        super.set(value);
        var builder = cast(_component._compositeBuilder, ScrollViewBuilder);
        builder.checkEmptyContentsComponent();
    }
}


@:dox(hide) @:noCompletion
@:access(haxe.ui.core.Component)
@:access(haxe.ui.containers.ScrollViewBuilder)
private class EmptyContentsText extends DefaultBehaviour {
    public override function set(value:Variant):Void {
        super.set(value);
        var scrollview = cast(_component, ScrollView);
        var emptyContentsComponent = scrollview.emptyContentsComponent;
        if (emptyContentsComponent == null) {
            var emptyContentsComponent = scrollview.findComponent("empty-contents-component", false, "css");
            if (emptyContentsComponent == null) {
                emptyContentsComponent = new Label();
                emptyContentsComponent.addClass("empty-contents-component");
                emptyContentsComponent.text = value;
                scrollview.emptyContentsComponent = emptyContentsComponent;
            }
        } else if ((emptyContentsComponent is Label)) {
            emptyContentsComponent.text = value;
        } else {
            var label = emptyContentsComponent.findComponent(Label, true);
            if (label != null) {
                label.text = value;
            }
        }
    }
}

//***********************************************************************************************************
// Events
//***********************************************************************************************************
@:dox(hide) @:noCompletion
typedef Inertia = {
    var screen:Point;
    var target:Point;
    var amplitude:Point;
    var direction:Point;
    var timestamp:Float;
}

@:dox(hide) @:noCompletion
@:access(haxe.ui.backend.ComponentImpl)
class ScrollViewEvents extends haxe.ui.events.Events {
    private var _scrollview:ScrollView;

    public function new(scrollview:ScrollView) {
        super(scrollview);
        _scrollview = scrollview;
    }

    public override function register() {
        var contents:Component = _scrollview.findComponent("scrollview-contents", false, "css");
        if (contents != null && contents.hasEvent(UIEvent.RESIZE, onContentsResized) == false) {
            contents.registerEvent(UIEvent.RESIZE, onContentsResized);
        }

        var hscroll:HorizontalScroll = _scrollview.findComponent(HorizontalScroll, false);
        if (hscroll != null && hscroll.hasEvent(UIEvent.CHANGE, onHScroll) == false) {
            hscroll.registerEvent(UIEvent.CHANGE, onHScroll);
        }
        if (hscroll != null && hscroll.hasEvent(ScrollEvent.SCROLL, onHScrollScroll) == false) {
            hscroll.registerEvent(ScrollEvent.SCROLL, onHScrollScroll);
        }

        var vscroll:VerticalScroll = _scrollview.findComponent(VerticalScroll, false);
        if (vscroll != null && vscroll.hasEvent(UIEvent.CHANGE, onVScroll) == false) {
            vscroll.registerEvent(UIEvent.CHANGE, onVScroll);
        }
        if (vscroll != null && vscroll.hasEvent(ScrollEvent.SCROLL, onVScrollScroll) == false) {
            vscroll.registerEvent(ScrollEvent.SCROLL, onVScrollScroll);
        }

        if (_scrollview.scrollMode == ScrollMode.DEFAULT || _scrollview.scrollMode == ScrollMode.DRAG || _scrollview.scrollMode == ScrollMode.INERTIAL || _scrollview.isHybridScroller) {
            registerEvent(MouseEvent.MIDDLE_MOUSE_DOWN, onMiddleMouseDown);
            registerEvent(MouseEvent.MOUSE_DOWN, onLeftMouseDown);
            registerEvent(MouseEvent.RIGHT_MOUSE_DOWN, onRightMouseDown);
        } else if (hasEvent(MouseEvent.MOUSE_DOWN, onLeftMouseDown)) {
            unregisterEvent(MouseEvent.MIDDLE_MOUSE_DOWN, onMiddleMouseDown);
            unregisterEvent(MouseEvent.MOUSE_DOWN, onLeftMouseDown);
            unregisterEvent(MouseEvent.RIGHT_MOUSE_DOWN, onRightMouseDown);
        }

        if (_scrollview.hasEvent(UIEvent.SHOWN) == false) {
            registerEvent(UIEvent.SHOWN, onShown);
        }

        if (_scrollview.hasEvent(UIEvent.COMPONENT_ADDED) == false) {
            registerEvent(UIEvent.COMPONENT_ADDED, onComponentAdded);
        }

        if (contents != null && contents.hasEvent(UIEvent.COMPONENT_ADDED) == false) {
            contents.registerEvent(UIEvent.COMPONENT_ADDED, onContentsComponentAdded);
        }

        if (_scrollview.hasEvent(UIEvent.COMPONENT_REMOVED) == false) {
            registerEvent(UIEvent.COMPONENT_REMOVED, onComponentRemoved);
        }

        if (contents != null && contents.hasEvent(UIEvent.COMPONENT_REMOVED) == false) {
            contents.registerEvent(UIEvent.COMPONENT_REMOVED, onComponentRemoved);
        }

        registerEvent(MouseEvent.MOUSE_WHEEL, onMouseWheel, Priority.LOW);
        registerEvent(ActionEvent.ACTION_START, onActionStart, Priority.LOW);
    }

    public override function unregister() {
        var contents:Component = _scrollview.findComponent("scrollview-contents", false, "css");
        if (contents != null) {
            contents.unregisterEvent(UIEvent.RESIZE, onContentsResized);
        }

        var hscroll:HorizontalScroll = _scrollview.findComponent(HorizontalScroll, false);
        if (hscroll != null) {
            hscroll.unregisterEvent(UIEvent.CHANGE, onHScroll);
            hscroll.unregisterEvent(ScrollEvent.SCROLL, onHScrollScroll);
        }

        var vscroll:VerticalScroll = _scrollview.findComponent(VerticalScroll, false);
        if (vscroll != null) {
            vscroll.unregisterEvent(UIEvent.CHANGE, onVScroll);
            vscroll.unregisterEvent(ScrollEvent.SCROLL, onVScrollScroll);
        }

        unregisterEvent(MouseEvent.MIDDLE_MOUSE_DOWN, onMiddleMouseDown);
        unregisterEvent(MouseEvent.RIGHT_MOUSE_DOWN, onRightMouseDown);
        unregisterEvent(MouseEvent.MOUSE_DOWN, onLeftMouseDown);
        unregisterEvent(MouseEvent.MOUSE_WHEEL, onMouseWheel);
        unregisterEvent(UIEvent.SHOWN, onShown);
        unregisterEvent(UIEvent.COMPONENT_ADDED, onComponentAdded);
        if (contents != null) {
            contents.unregisterEvent(UIEvent.COMPONENT_ADDED, onContentsComponentAdded);
        }
        unregisterEvent(UIEvent.COMPONENT_REMOVED, onComponentRemoved);
        if (contents != null) {
            contents.unregisterEvent(UIEvent.COMPONENT_REMOVED, onContentsComponentRemoved);
        }
        unregisterEvent(ActionEvent.ACTION_START, onActionStart);
    }

    private function onShown(event:UIEvent) {
        _scrollview.invalidateComponentLayout();
        var hscroll:HorizontalScroll = _scrollview.findComponent(HorizontalScroll, false);
        if (hscroll != null) {
            hscroll.invalidateComponentLayout();
        }
        var vscroll:VerticalScroll = _scrollview.findComponent(VerticalScroll, false);
        if (vscroll != null) {
            vscroll.invalidateComponentLayout();
        }
    }

    private function onComponentAdded(event:UIEvent) { 
        if ((event.relatedComponent is Scroll)) {
            event.cancel();
            var scrollEvent = new ScrollEvent(ScrollEvent.CHANGE);
            _scrollview.dispatch(scrollEvent);
        }
    }

    private function onContentsComponentAdded(event:UIEvent) { 
        _scrollview.dispatch(event);
    }

    private function onComponentRemoved(event:UIEvent) { 
        if ((event.relatedComponent is Scroll)) {
            event.cancel();
            var scrollEvent = new ScrollEvent(ScrollEvent.CHANGE);
            _scrollview.dispatch(scrollEvent);
        }
    }

    private function onContentsComponentRemoved(event:UIEvent) { 
        _scrollview.dispatch(event);
    }

    private function onContentsResized(event:UIEvent) {
        _scrollview.invalidateComponent(InvalidationFlags.SCROLL);
    }

    private function onHScroll(event:UIEvent) {
        _scrollview.invalidateComponent(InvalidationFlags.SCROLL);
        _target.dispatch(new ScrollEvent(ScrollEvent.CHANGE));
    }

    private function onHScrollScroll(event:UIEvent) {
        _target.dispatch(new ScrollEvent(ScrollEvent.SCROLL));
    }

    private function onVScroll(event:UIEvent) {
        _scrollview.invalidateComponent(InvalidationFlags.SCROLL);
        _target.dispatch(new ScrollEvent(ScrollEvent.CHANGE));
    }

    private function onVScrollScroll(event:UIEvent) {
        _target.dispatch(new ScrollEvent(ScrollEvent.SCROLL));
    }

    @:access(haxe.ui.core.Component)
    private function onLeftMouseDown(event:MouseEvent) {
        if (_scrollview.scrollMouseButton == MouseButton.LEFT) {
            onMouseDown(event);
        }
    }

    @:access(haxe.ui.core.Component)
    private function onMiddleMouseDown(event:MouseEvent) {
        if (_scrollview.scrollMouseButton == MouseButton.MIDDLE) {
            onMouseDown(event);
        }
    }

    @:access(haxe.ui.core.Component)
    private function onRightMouseDown(event:MouseEvent) {
        if (_scrollview.scrollMouseButton == MouseButton.RIGHT) {
            onMouseDown(event);
        }
    }
    
    private var _offset:Point;
    private static inline var INERTIAL_TIME_CONSTANT:Int = 325;
    private var _inertia:Inertia = null;
    @:access(haxe.ui.core.Component)
    private function onMouseDown(event:MouseEvent) {
        var hscroll:HorizontalScroll = _scrollview.findComponent(HorizontalScroll, false);
        var vscroll:VerticalScroll = _scrollview.findComponent(VerticalScroll, false);

        if (hscroll == null && vscroll == null) {
            return;
        }

        _scrollview.addClass(":down");

        _lastMousePos = new Point(event.screenX, event.screenY);

        var componentOffset = _scrollview.getComponentOffset();
        // we want to disallow mouse scrolling if we are under a textfield/textarea as this stops selection of data in textfield
        // if we are under a scrollbar, lets let the scroll bar handle it (rather than scrollview intefering) 
        var under = _scrollview.findComponentsUnderPoint(event.screenX - componentOffset.x, event.screenY - componentOffset.y);
        for (c in under) {
            if (c.hasTextInput() || (c is Scroll)) {
                return;
            }
        }

        //event.cancel();

        _offset = new Point();
        if (hscroll != null) {
            _offset.x = hscroll.pos + event.screenX;
        }
        if (vscroll != null) {
            _offset.y = vscroll.pos + event.screenY;
        }

        if (_scrollview.scrollMode == ScrollMode.INERTIAL) {
            if (_inertia == null) {
                _inertia = {
                    screen: new Point(),
                    target: new Point(),
                    amplitude: new Point(),
                    direction: new Point(),
                    timestamp: 0
                }
            }

            _inertia.target.x = _scrollview.hscrollPos;
            _inertia.target.y = _scrollview.vscrollPos;
            _inertia.amplitude.x = 0;
            _inertia.amplitude.y = 0;

            _inertia.screen.x = event.screenX;
            _inertia.screen.y = event.screenY;

            _inertia.timestamp = haxe.Timer.stamp();
        }

        Screen.instance.registerEvent(MouseEvent.MOUSE_MOVE, onMouseMove);
        Screen.instance.registerEvent(MouseEvent.MIDDLE_MOUSE_UP, onMouseUp);
        Screen.instance.registerEvent(MouseEvent.RIGHT_MOUSE_UP, onMouseUp);
        Screen.instance.registerEvent(MouseEvent.MOUSE_UP, onMouseUp);
    }

    private var _movementThreshold:Int = 3;
    private var _lastMousePos:Point = null;
    private function onMouseMove(event:MouseEvent) {
        event.cancel();
        var hscroll:HorizontalScroll = _scrollview.findComponent(HorizontalScroll, false);
        if (hscroll != null) {
            hscroll.pos = _offset.x - event.screenX;
            var distX = Math.abs(event.screenX - _lastMousePos.x);
            #if haxeui_kha
            if (distX > 0) {
                pauseContainerEvents();
            }
            #else
            if (distX > Toolkit.scaleX) {
                pauseContainerEvents();
            }
            #end
        }
        var vscroll:VerticalScroll = _scrollview.findComponent(VerticalScroll, false);
        if (vscroll != null) {
            vscroll.pos = _offset.y - event.screenY;
            var distY = Math.abs(event.screenY - _lastMousePos.y);
            #if haxeui_kha
            if (distY > 0) {
                pauseContainerEvents();
            }
            #else
            if (distY > Toolkit.scaleY) {
                pauseContainerEvents();
            }
            #end
        }
        _lastMousePos = new Point(event.screenX, event.screenY);
    }

    private var _containerEventsPaused:Bool = false;
    private function pauseContainerEvents() {
        if (_containerEventsPaused == true) {
            return;
        }
        _containerEventsPaused = true;
        onContainerEventsStatusChanged();
    }

    private function resumeContainerEvents() {
        if (_containerEventsPaused == false) {
            return;
        }

        _containerEventsPaused = false;
        onContainerEventsStatusChanged();
    }

    @:access(haxe.ui.core.Component)
    private function onContainerEventsStatusChanged() {
        var scrollViewContents = _scrollview.findComponent("scrollview-contents", Component, true, "css");
        scrollViewContents.disableInteractivity(_containerEventsPaused);

        if (_containerEventsPaused == true) {
            scrollViewContents.removeClass(":hover", true, true);
        } else {
            var components = scrollViewContents.findComponentsUnderPoint(Screen.instance.currentMouseX, Screen.instance.currentMouseY);
            for (c in components) {
                var mouseEvent = new MouseEvent(MouseEvent.MOUSE_OVER);
                c.dispatch(mouseEvent);
            }
        }

        var hscroll = _scrollview.findComponent(HorizontalScroll, false);
        var vscroll = _scrollview.findComponent(VerticalScroll, false);
        if (hscroll != null || vscroll != null) {
            if (_scrollview.autoHideScrolls == true) {
                if (_containerEventsPaused == true) {
                    if (hscroll != null) {
                        //hscroll.hidden = false;
                        hscroll.fadeIn();
                    }
                    if (vscroll != null) {
                        //vscroll.hidden = false;
                        vscroll.fadeIn();
                    }
                } else {
                    if (hscroll != null) {
                        //hscroll.hidden = true;
                        hscroll.fadeOut();
                    }
                    if (vscroll != null) {
                        //vscroll.hidden = true;
                        vscroll.fadeOut();
                    }
                }
            }
        }
    }

    private function onMouseUp(event:MouseEvent) {
        Screen.instance.unregisterEvent(MouseEvent.MOUSE_MOVE, onMouseMove);
        Screen.instance.unregisterEvent(MouseEvent.MIDDLE_MOUSE_UP, onMouseUp);
        Screen.instance.unregisterEvent(MouseEvent.RIGHT_MOUSE_UP, onMouseUp);
        Screen.instance.unregisterEvent(MouseEvent.MOUSE_UP, onMouseUp);

        if (_scrollview.scrollMode == ScrollMode.INERTIAL) {
            var now = haxe.Timer.stamp();
            var elapsed = (now - _inertia.timestamp) * 1000;

            var deltaX = Math.abs(_inertia.screen.x - event.screenX);
            var deltaY = Math.abs(_inertia.screen.y - event.screenY);

            _inertia.direction.x = (_inertia.screen.x - event.screenX) < 0 ? 0 : 1;
            var velocityX = deltaX / elapsed;
            var v = 1000 * deltaX / (1 + elapsed);
            velocityX = 0.8 * v + 0.2 * velocityX;

            _inertia.direction.y = (_inertia.screen.y - event.screenY) < 0 ? 0 : 1;
            var velocityY = deltaY / elapsed;
            var v = 1000 * deltaY / (1 + elapsed);
            velocityY = 0.8 * v + 0.2 * velocityY;

            if (velocityX <= 75 && velocityY <= 75) {
                dispatch(new ScrollEvent(ScrollEvent.STOP));
                Toolkit.callLater(resumeContainerEvents);
                return;
            }

            _inertia.timestamp = haxe.Timer.stamp();

            var hscroll:HorizontalScroll = _scrollview.findComponent(HorizontalScroll, false);
            if (hscroll != null) {
                _inertia.amplitude.x = 0.8 * velocityX;
            }
            if (_inertia.direction.x == 0) {
                _inertia.target.x = Math.round(_scrollview.hscrollPos - _inertia.amplitude.x);
            } else {
                _inertia.target.x = Math.round(_scrollview.hscrollPos + _inertia.amplitude.x);
            }

            var vscroll:VerticalScroll = _scrollview.findComponent(VerticalScroll, false);
            if (vscroll != null) {
                _inertia.amplitude.y = 0.8 * velocityY;
            }
            if (_inertia.direction.y == 0) {
                _inertia.target.y = Math.round(_scrollview.vscrollPos - _inertia.amplitude.y);
            } else {
                _inertia.target.y = Math.round(_scrollview.vscrollPos + _inertia.amplitude.y);
            }

            if (_scrollview.hscrollPos == _inertia.target.x && _scrollview.vscrollPos == _inertia.target.y) {
                dispatch(new ScrollEvent(ScrollEvent.STOP));
                Toolkit.callLater(resumeContainerEvents);
                return;
            }

            if (_scrollview.hscrollPos == _inertia.target.x) {
                _inertia.amplitude.x = 0;
            }
            if (_scrollview.vscrollPos == _inertia.target.y) {
                _inertia.amplitude.y = 0;
            }

            Toolkit.callLater(inertialScroll);
        } else {
            _scrollview.removeClass(":down");
            dispatch(new ScrollEvent(ScrollEvent.STOP));
            Toolkit.callLater(resumeContainerEvents);
        }
    }

    private function inertialScroll() {
        var elapsed = (haxe.Timer.stamp() - _inertia.timestamp) * 1000;

        var finishedX = false;
        if (_inertia.amplitude.x != 0) {
            var deltaX = -_inertia.amplitude.x * Math.exp(-elapsed / INERTIAL_TIME_CONSTANT);
            if (deltaX > 0.5 || deltaX < -0.5) {
                var oldPos = _scrollview.hscrollPos;
                var newPos:Float = 0;
                if (_inertia.direction.x == 0) {
                    newPos = _inertia.target.x - deltaX;
                } else {
                    newPos = _inertia.target.x + deltaX;
                }
                if (newPos < 0) {
                    newPos = 0;
                } else if (newPos > _scrollview.hscrollMax) {
                    newPos = _scrollview.hscrollMax;
                }
                _scrollview.hscrollPos = newPos;
                finishedX = (newPos == oldPos || newPos == 0 || newPos == _scrollview.hscrollMax);
            } else {
                finishedX = true;
            }
        } else {
            finishedX = true;
        }

        var finishedY = false;
        if (_inertia.amplitude.y != 0) {
            var deltaY = -_inertia.amplitude.y * Math.exp(-elapsed / INERTIAL_TIME_CONSTANT);
            if (deltaY > 0.5 || deltaY < -0.5) {
                var oldPos = _scrollview.vscrollPos;
                var newPos:Float = 0;
                if (_inertia.direction.y == 0) {
                    newPos = _inertia.target.y - deltaY;
                } else {
                    newPos = _inertia.target.y + deltaY;
                }
                if (newPos < 0) {
                    newPos = 0;
                } else if (newPos > _scrollview.vscrollMax) {
                    newPos = _scrollview.vscrollMax;
                }
                _scrollview.vscrollPos = newPos;
                finishedY = (newPos == oldPos || newPos == 0 || newPos == _scrollview.vscrollMax);
            } else {
                finishedY = true;
            }
        } else {
            finishedY = true;
        }

        if (finishedX == true && finishedY == true) {
            dispatch(new ScrollEvent(ScrollEvent.STOP));
            Toolkit.callLater(resumeContainerEvents);
        } else {
            Toolkit.callLater(inertialScroll);
        }
    }

    private var _fadeTimer:Timer = null;
    @:access(haxe.ui.core.Component)
    private function onMouseWheel(event:MouseEvent) {
        if (_scrollview.isHybridScroller) {
            if (_scrollview.scrollPolicy == ScrollPolicy.NEVER) {
                return;
            }
            var primaryType:Class<Scroll> = VerticalScroll;
            var secondaryType:Class<Scroll> = HorizontalScroll;
            if (event.shiftKey) {
                primaryType = HorizontalScroll;
                secondaryType = VerticalScroll;
            }
            var scroll:Scroll = _scrollview.findComponent(primaryType, false);
            if (scroll == null) {
                scroll = _scrollview.findComponent(secondaryType, false);
            }
    
            if (_scrollview.autoHideScrolls == true && _fadeTimer == null) {
                scroll.fadeIn();
            }
            if (_scrollview.autoHideScrolls == true) {
                if (_fadeTimer != null) {
                    _fadeTimer.stop();
                    _fadeTimer = null;
                }
                _fadeTimer = new Timer(300, function() {
                    scroll.fadeOut();
                    _fadeTimer.stop();
                    _fadeTimer = null;
                });
            }
            return;
        }

        // we'll default to vertical scrolling for the mouse wheel, however,
        // if there is no vertical scrollbar we'll try to use horizontal
        // scrolling instead - note that if the shiftkey is pressed
        // we'll reverse that an look primarily to scroll horizontally
        if (_scrollview.scrollPolicy == ScrollPolicy.NEVER) {
            return;
        }
        var primaryType:Class<Scroll> = VerticalScroll;
        var secondaryType:Class<Scroll> = HorizontalScroll;
        if (event.shiftKey) {
            primaryType = HorizontalScroll;
            secondaryType = VerticalScroll;
        }
        var scroll:Scroll = _scrollview.findComponent(primaryType, false);
        if (scroll == null) {
            scroll = _scrollview.findComponent(secondaryType, false);
        }

        if (scroll != null) {
            var currentScrollPolicy = scroll.id == 'scrollview-vscroll'
            ? _scrollview.verticalScrollPolicy
            : _scrollview.horizontalScrollPolicy;

            if (currentScrollPolicy == ScrollPolicy.NEVER) {
              return;
            }

            if (_scrollview.autoHideScrolls == true && _fadeTimer == null) {
                scroll.fadeIn();
            }
            event.cancel();
            var amount = 50; // TODO: calculate this
            #if haxeui_pdcurses
            amount = 2;
            #end
            if (event.delta > 0) {
                scroll.pos -= amount;
            } else if (event.delta < 0) {
                scroll.pos += amount;
            }
            if (_scrollview.autoHideScrolls == true) {
                if (_fadeTimer != null) {
                    _fadeTimer.stop();
                    _fadeTimer = null;
                }
                _fadeTimer = new Timer(300, function() {
                    scroll.fadeOut();
                    _fadeTimer.stop();
                    _fadeTimer = null;
                });
            }
        }
    }
    
    private function onActionStart(event:ActionEvent) {
        if (_scrollview.scrollPolicy == ScrollPolicy.NEVER) {
            return;
        }
        switch (event.action) {
            case ActionType.DOWN:
                if (_scrollview.verticalScrollPolicy != ScrollPolicy.NEVER) {
                    _scrollview.vscrollPos++;
                    event.repeater = true;
                }
            case ActionType.UP:
                if (_scrollview.verticalScrollPolicy != ScrollPolicy.NEVER) {
                    _scrollview.vscrollPos--;
                    event.repeater = true;
                }
            case ActionType.LEFT:
                if (_scrollview.horizontalScrollPolicy != ScrollPolicy.NEVER) {
                    _scrollview.hscrollPos--;
                    event.repeater = true;
                }
            case ActionType.RIGHT:
                if (_scrollview.horizontalScrollPolicy != ScrollPolicy.NEVER) {
                    _scrollview.hscrollPos++;
                    event.repeater = true;
                }
            case _:
        }
    }
}

//***********************************************************************************************************
// Composite Builder
//***********************************************************************************************************
@:dox(hide) @:noCompletion
@:allow(haxe.ui.containers.ScrollView)
@:access(haxe.ui.core.Component)
class ScrollViewBuilder extends CompositeBuilder {
    private var _scrollview:ScrollView;
    private var _contents:Box;
    private var _contentsLayoutName:String;

    public function new(scrollview:ScrollView) {
        super(scrollview);
        _scrollview = scrollview;
        _scrollview.cascadeActive = true;
    }

    public override function create() {
        var contentLayoutName = _scrollview.contentLayoutName;
        if (contentLayoutName == null) {
            contentLayoutName = "vertical";
        }
        createContentContainer(contentLayoutName);
    }

    public override function destroy() {
    }

    private function checkEmptyContentsComponent(contentsComponent:Component = null) {
        if (_contents == null) {
            return;
        }

        var emptyContentsComponent:Component = _scrollview.emptyContentsComponent;
        if (emptyContentsComponent == null) {
            return;
        }

        if (contentsComponent == null) {
            contentsComponent = _contents;
        }

        var containsEmptyContentsComponent = _scrollview.containsChildComponent(emptyContentsComponent);
        if (contentsComponent.numComponents == 0) {
            if (!containsEmptyContentsComponent) {
                emptyContentsComponent.addClass("empty-contents-component");
                _scrollview.addComponent(emptyContentsComponent);
            }

            if (_scrollview.emptyContentsText != null) {
                if ((emptyContentsComponent is Label)) {
                    emptyContentsComponent.text = _scrollview.emptyContentsText;
                } else {
                    var label = emptyContentsComponent.findComponent(Label, true);
                    if (label != null) {
                        label.text = _scrollview.emptyContentsText;
                    }
                }                
            }

            emptyContentsComponent.show();
        } else if (containsEmptyContentsComponent) {
            emptyContentsComponent.hide();
        }
    }

    private override function get_numComponents():Null<Int> {
        return _contents.numComponents;
    }

    public override function addComponent(child:Component):Component {
        if (_scrollview.emptyContentsComponent == null && (child.id == "emptyContentsComponent" || child.hasClass("empty-contents-component"))) {
            _scrollview.emptyContentsComponent = child;
            return child;
        } else if (_scrollview.emptyContentsComponent != null && (child.id == "emptyContentsComponent" || child.hasClass("empty-contents-component")) && child != _scrollview.emptyContentsComponent) {
            _scrollview.removeComponent(_scrollview.emptyContentsComponent);
            _scrollview.emptyContentsComponent = child;
            return child;
        }

        if (_scrollview.emptyContentsComponent != null && child == _scrollview.emptyContentsComponent) {
            return null;
        }
        if ((child is HorizontalScroll) == false && (child is VerticalScroll) == false && child.hasClass("scrollview-contents") == false) {
            var contentsComponent = null;
            if ((child is Box)) {
                child.registerEvent(UIEvent.COMPONENT_ADDED, onContentsChanged);
                child.registerEvent(UIEvent.COMPONENT_REMOVED, onContentsChanged);
                contentsComponent = child;
            }
            var r = _contents.addComponent(child);
            checkEmptyContentsComponent(contentsComponent);
            return r;
        }
        return null;
    }

    public override function addComponentAt(child:Component, index:Int):Component {
        if ((child is HorizontalScroll) == false && (child is VerticalScroll) == false && child.hasClass("scrollview-contents") == false) {
            var r = _contents.addComponentAt(child, index);
            checkEmptyContentsComponent();
            return r;
        }
        return null;
    }

    public override function removeComponent(child:Component, dispose:Bool = true, invalidate:Bool = true):Component {
        if (_scrollview.emptyContentsComponent != null && child == _scrollview.emptyContentsComponent) {
            return null;
        }
        if ((child is HorizontalScroll) == false && (child is VerticalScroll) == false && child.hasClass("scrollview-contents") == false) {
            var r = _contents.removeComponent(child, dispose, invalidate);
            checkEmptyContentsComponent();
            return r;
        }
        return null;
    }

    public override function removeComponentAt(index:Int, dispose:Bool = true, invalidate:Bool = true):Component {
        var r = _contents.removeComponentAt(index, dispose, invalidate);
        checkEmptyContentsComponent();
        return r;
    }

    public override function removeAllComponents(dispose:Bool = true) {
        _contents.removeAllComponents(dispose);
        checkEmptyContentsComponent();
    }
    
    public override function getComponentIndex(child:Component):Int {
        return _contents.getComponentIndex(child);
    }

    public override function setComponentIndex(child:Component, index:Int):Component {
        if ((child is HorizontalScroll) == false && (child is VerticalScroll) == false && child.hasClass("scrollview-contents") == false) {
            return _contents.setComponentIndex(child, index);
        }
        return null;
    }

    public override function getComponentAt(index:Int):Component {
        return _contents.getComponentAt(index);
    }

    private function createContentContainer(layoutName:String) {
        if (_contents == null) {
            _contents = new Box();
            _contents.registerEvent(UIEvent.COMPONENT_ADDED, onContentsChanged);
            _contents.registerEvent(UIEvent.COMPONENT_REMOVED, onContentsChanged);
            _contents.addClass("scrollview-contents");
            _contents.id = "scrollview-contents";
            _contents.layout = LayoutFactory.createFromName(layoutName); // TODO: temp
            _component.addComponent(_contents);
            _contentsLayoutName = layoutName;
        }
    }

    private function onContentsChanged(event:UIEvent) { 
        checkEmptyContentsComponent(event.target);
    }

    private function horizontalConstraintModifier():Float {
        return 0;
    }

    private function verticalConstraintModifier():Float {
        return 0;
    }

    @:access(haxe.ui.backend.ComponentBase)
    private function checkScrolls() {
        if (_component.isNativeScroller == true) {
            return;
        }

        var usableSize:Size = _component.layout.usableSize;

        if (virtualHorizontal == false && usableSize.width > 0) {
            var horizontalConstraint = _contents;
            var hscroll:HorizontalScroll = _component.findComponent(HorizontalScroll, false);
            var vcw:Float = horizontalConstraint.width + horizontalConstraintModifier();
            if (vcw > usableSize.width) {
                if (hscroll == null) {
                    hscroll = createHScroll();
                }

                hscroll.max = vcw - usableSize.width;
                if (_scrollview.hscrollThumbSize == null) {
                    hscroll.pageSize = (usableSize.width / vcw) * hscroll.max;
                }

                hscroll.syncComponentValidation();    //avoid another pass
            } else if (_scrollview.horizontalScrollPolicy == ScrollPolicy.ALWAYS) {
                if (hscroll == null) {
                    hscroll = createHScroll();
                }
                hscroll.max = 0;
                hscroll.pageSize = 0;
            } else {
                if (_scrollview.horizontalScrollPolicy != ScrollPolicy.ALWAYS && hscroll != null) {
                    destroyHScroll();
                }
            }
        }

        if (virtualVertical == false && usableSize.height > 0) {
            var verticalConstraint = _contents;
            var vscroll:VerticalScroll = _component.findComponent(VerticalScroll, false);
            var vch:Float = verticalConstraint.height + verticalConstraintModifier();
            if (vch > usableSize.height) {
                if (vscroll == null) {
                    vscroll = createVScroll();
                }

                vscroll.max = vch - usableSize.height;
                if (_scrollview.vscrollThumbSize == null) {
                    vscroll.pageSize = (usableSize.height / vch) * vscroll.max;
                }

                vscroll.syncComponentValidation();    //avoid another pass
            } else if (_scrollview.verticalScrollPolicy == ScrollPolicy.ALWAYS) {
                if (vscroll == null) {
                    vscroll = createVScroll();
                }
                vscroll.max = 0;
                vscroll.pageSize = 0;
            } else {
                if (_scrollview.verticalScrollPolicy != ScrollPolicy.ALWAYS && vscroll != null) {
                    destroyVScroll();
                }
            }
        }
    }

    @:access(haxe.ui.backend.ComponentBase)
    public function createHScroll():HorizontalScroll {
        if (_component.isNativeScroller == true) {
            return null;
        }

        var usableSize:Size = _component.layout.usableSize;
        var horizontalConstraint = _contents;
        var hscroll:HorizontalScroll = _component.findComponent(HorizontalScroll, false);
        var vcw:Float = horizontalConstraint.width + horizontalConstraintModifier();

        if (usableSize.width <= 0) {
            return hscroll;
        }

        if (vcw > usableSize.width && hscroll == null || _scrollview.horizontalScrollPolicy == ScrollPolicy.ALWAYS) {
            hscroll = new HorizontalScroll();
            hscroll.scriptAccess = false;
            hscroll.includeInLayout = !_scrollview.autoHideScrolls;
            hscroll.hidden = _scrollview.autoHideScrolls;
            hscroll.percentWidth = 100;
            hscroll.allowFocus = false;
            hscroll.id = "scrollview-hscroll";
            if (_scrollview.hscrollThumbSize != null) {
                hscroll.thumbSize = _scrollview.hscrollThumbSize;
            }
            _component.addComponent(hscroll);
            _component.registerInternalEvents(true);
        }

        if (_scrollview.horizontalScrollPolicy == ScrollPolicy.NEVER) {
            hscroll.includeInLayout = false;
            hscroll.hidden = true;
        } else if (_scrollview.horizontalScrollPolicy == ScrollPolicy.ALWAYS) {
            hscroll.includeInLayout = true;
            hscroll.hidden = false;
        }

        return hscroll;
    }

    @:access(haxe.ui.backend.ComponentBase)
    public function createVScroll():VerticalScroll {
        if (_component.isNativeScroller == true) {
            return null;
        }

        var usableSize:Size = _component.layout.usableSize;
        var verticalConstraint = _contents;
        var vscroll:VerticalScroll = _component.findComponent(VerticalScroll, false);
        var vch:Float = verticalConstraint.height + verticalConstraintModifier();

        if (usableSize.height <= 0) {
            return vscroll;
        }

        if ((vch > usableSize.height && vscroll == null) || _scrollview.verticalScrollPolicy == ScrollPolicy.ALWAYS) {
            vscroll = new VerticalScroll();
            vscroll.scriptAccess = false;
            vscroll.includeInLayout = !_scrollview.autoHideScrolls;
            vscroll.hidden = _scrollview.autoHideScrolls;
            vscroll.percentHeight = 100;
            vscroll.allowFocus = false;
            vscroll.id = "scrollview-vscroll";
            if (_scrollview.vscrollThumbSize != null) {
                vscroll.thumbSize = _scrollview.vscrollThumbSize;
            }
            _component.addComponent(vscroll);
            _component.registerInternalEvents(true);
        }

        if (_scrollview.verticalScrollPolicy == ScrollPolicy.NEVER) {
            vscroll.includeInLayout = false;
            vscroll.hidden = true;
        } else if (_scrollview.verticalScrollPolicy == ScrollPolicy.ALWAYS) {
            vscroll.includeInLayout = true;
            vscroll.hidden = false;
        }

        return vscroll;
    }

    @:access(haxe.ui.backend.ComponentBase)
    public function destroyHScroll() {
        var hscroll:HorizontalScroll = _component.findComponent(HorizontalScroll, false);
        if (hscroll != null) {
            _component.removeComponent(hscroll);
        }
    }

    @:access(haxe.ui.backend.ComponentBase)
    public function destroyVScroll() {
        var vscroll:VerticalScroll = _component.findComponent(VerticalScroll, false);
        if (vscroll != null) {
            _component.removeComponent(vscroll);
        }
    }

    private function updateScrollRect() {
        if (_contents == null) {
            return;
        }

        var usableSize = _component.layout.usableSize;

        var clipCX = usableSize.width - horizontalConstraintModifier();
        if (clipCX > _contents.width) {
            clipCX = _contents.width + horizontalConstraintModifier();
        }
        var clipCY = usableSize.height - verticalConstraintModifier();
        if (clipCY > _contents.height) {
            clipCY = _contents.height + verticalConstraintModifier();
        }

        var xpos:Float = 0;
        var ypos:Float = 0;

        if (virtualHorizontal == false) {
            var hscroll = _component.findComponent(HorizontalScroll, false);
            if (hscroll != null) {
                xpos = hscroll.pos;
            }
        } else if (_contents.componentClipRect != null) {
            clipCX = _contents.componentClipRect.width;
        }

        if (virtualVertical == false) {
            var vscroll = _component.findComponent(VerticalScroll, false);
            if (vscroll != null) {
                ypos = vscroll.pos;
            }
        } else if (_contents.componentClipRect != null) {
            clipCY = _contents.componentClipRect.height;
        }

        var newClipRect:Rectangle = new Rectangle(Math.fround(xpos), Math.fround(ypos), Math.fround(clipCX), Math.fround(clipCY));
        _contents.componentClipRect = newClipRect;
        _contents.walkComponents(function(c) {
            // we dont usually check see if a component has an event before dispatching it
            // however, in this specific case we are going to, potentially, be disaptching
            // a move event for many child components, so lets just not do that if we know
            // the component isnt going to respond to that event anyway
            if (c.hasEvent(UIEvent.MOVE)) {
                c.dispatch(new UIEvent(UIEvent.MOVE));
            }
            return true;
        });
    }

    public var virtualHorizontal(get, null):Bool;
    private function get_virtualHorizontal():Bool {
        return _scrollview.virtual;
    }

    public var virtualVertical(get, null):Bool;
    private function get_virtualVertical():Bool {
        return _scrollview.virtual;
    }

    public function onVirtualChanged() {

    }

    public override function applyStyle(style:Style) {
        super.applyStyle(style);
        if (style.mode == "mobile") {
            _scrollview.autoHideScrolls = true;
        }
        
        if (style.contentWidth != null && style.contentWidth != _scrollview.contentWidth) {
            _scrollview.contentWidth = style.contentWidth;
        } else if (style.contentWidthPercent != null && style.contentWidthPercent != _scrollview.percentContentWidth) {
            _scrollview.percentContentWidth = style.contentWidthPercent;
        }
        
        if (style.contentHeight != null && style.contentHeight != _scrollview.contentHeight) {
            _scrollview.contentHeight = style.contentHeight;
        } else if (style.contentHeightPercent != null && style.contentHeightPercent != _scrollview.percentContentHeight) {
            _scrollview.percentContentHeight = style.contentHeightPercent;
        }
    }
}