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}