001package polaris.sdk.ui.controls; 002import polaris.sdk.ui.*; 003/** Rounded content clipping with a scrollbar gutter only when content overflows. */ 004public class ScrollPane extends Panel { 005 protected float scrollY,contentHeight;private float shownScroll;private boolean snapReveal,dragging;private float grab; 006 private static final float GUTTER=12,TRACK=7; 007 public ScrollPane(){clipChildren=true;w=320;h=180;} 008 public void reveal(Widget widget){snapReveal=true;float top=widget.absoluteY(),bottom=top+widget.height();if(top<ay+padding)scroll(scrollY+top-ay-padding);else if(bottom>ay+h-padding)scroll(scrollY+bottom-ay-h+padding);} 009 public float scroll(){return scrollY;} 010 public ScrollPane scroll(float value){scrollY=Math.max(0,Math.min(maxScroll(),value));return this;} 011 protected float maxScroll(){return Math.max(0,contentHeight+padding-h);} 012 public boolean hasScrollbar(){return maxScroll()>.001f;} 013 /** Layout width, with equal base insets and a conditional scrollbar gutter. */ 014 public float contentWidth(){return Math.max(0,w-2*padding-(hasScrollbar()?GUTTER:0));} 015 @Override protected float offsetY(){return -shownScroll;} 016 @Override protected Rect childClip(){return clip.intersect(new Rect(ax,ay,Math.max(0,w-(hasScrollbar()?padding+GUTTER:0)),h));} 017 @Override protected void layoutChildren(){ 018 float outerWidth=w;boolean before=hasScrollbar(); 019 try{if(before)w=Math.max(0,w-GUTTER);super.layoutChildren();}finally{w=outerWidth;} 020 contentHeight=0;for(Widget c:children)if(c.visible())contentHeight=Math.max(contentHeight,c.y()+c.height()); 021 // Updating the gutter does not leave stale wide content after resizing. 022 if(before!=hasScrollbar()){try{if(hasScrollbar())w=Math.max(0,w-GUTTER);super.layoutChildren();}finally{w=outerWidth;}} 023 scroll(scrollY);if(snapReveal){snapMotion("scroll",scrollY);shownScroll=scrollY;snapReveal=false;}else shownScroll=Math.max(0,Math.min(maxScroll(),follow("scroll",scrollY,dragging?72:36))); 024 } 025 @Override public Widget hit(float x,float y){if(visible&&enabled&&hasScrollbar()&&contains(x,y)&&x>=ax+w-padding-GUTTER)return this;return super.hit(x,y);} 026 private float trackHeight(){return Math.max(0,h-2*padding);} 027 private float trackX(){return ax+w-padding-TRACK;} 028 private float thumbLength(){float track=trackHeight();return Math.min(track,Math.max(24,track*h/Math.max(h,contentHeight+padding)));} 029 @Override protected void paintOverlay(Painter p){if(hasScrollbar()){ 030 pushContentClip(p);try{float track=trackHeight(),thumb=thumbLength();p.fill(trackX(),ay+padding,TRACK,track,TRACK/2,theme().field);p.fill(trackX(),ay+padding+(track-thumb)*shownScroll/maxScroll(),TRACK,thumb,TRACK/2,theme().selected);}finally{popContentClip(p);} 031 }} 032 private void dragTo(float py){float travel=Math.max(0,trackHeight()-thumbLength());scroll(travel>0?(py-ay-padding-grab)/travel*maxScroll():0);changed();} 033 @Override public void event(UiEvent e){ 034 if(e.type==UiEvent.Type.WHEEL){scroll(scrollY+e.wheel*30);e.handled=true;changed();} 035 else if(e.type==UiEvent.Type.DOWN&&e.button==1&&hasScrollbar()&&e.x>=ax+w-padding-GUTTER){float start=ay+padding+(trackHeight()-thumbLength())*shownScroll/maxScroll();boolean inside=e.y>=start&&e.y<=start+thumbLength();grab=inside?e.y-start:thumbLength()/2;dragging=true;pressed=true;if(!inside)dragTo(e.y);e.handled=true;} 036 else if((e.type==UiEvent.Type.MOVE||e.type==UiEvent.Type.UP&&e.button==1)&&dragging){dragTo(e.y);e.handled=true;} 037 if(e.type==UiEvent.Type.UP||e.type==UiEvent.Type.BLUR){dragging=false;pressed=false;} 038 } 039}