001package polaris.sdk.ui; 002import java.util.*; 003import java.util.function.*; 004import polaris.sdk.ui.controls.PopupMenu; 005/** Retained widget; all mutations/event callbacks take place on the Ui thread. */ 006public class Widget { 007 protected float x,y,w=180,h=36,ax,ay; protected Rect clip=new Rect(0,0,0,0); 008 protected String text="",id="",tooltip=""; protected boolean visible=true,enabled=true,focusable=false,clipChildren=false; 009 protected boolean hovered,pressed;protected Ui ui;protected Widget parent; 010 protected final List<Widget> children=new ArrayList<Widget>(); 011 private final Map<String,MotionValue> motions=new HashMap<String,MotionValue>(); 012 private boolean smoothPosition;private final MotionValue positionX=new MotionValue(),positionY=new MotionValue(); 013 private CornerRadii cornerProfile; 014 private Theme detachedTheme;private double lastTime,pressUntil,appearAt=-1;private boolean visualDown,pendingPulse; 015 protected Consumer<Widget> click,change;protected PopupMenu contextMenu; 016 public Widget(){} public Widget(String text){this.text=text==null?"":text;} 017 public Widget bounds(float x,float y,float w,float h){if(!finite(x)||!finite(y)||!finite(w)||!finite(h)||w<0||h<0)throw new IllegalArgumentException("Invalid widget bounds");this.x=x;this.y=y;this.w=w;this.h=h;return this;} 018 private static boolean finite(float f){return !Float.isNaN(f)&&!Float.isInfinite(f);} 019 /** Position follows its target while rendering and hit testing use the same shown geometry. */ 020 public Widget smoothPosition(boolean value){if(value&&!smoothPosition){positionX.snap(x,clock());positionY.snap(y,clock());}smoothPosition=value;return this;} 021 /** Grab an already moving widget at its shown location rather than at its old target. */ 022 public Widget beginPositionDrag(){if(smoothPosition){x=positionX.value();y=positionY.value();}smoothPosition(true);positionX.snap(x,clock());positionY.snap(y,clock());return this;} 023 /** Four independent cubic corner radii, clockwise from the top left. */ 024 public Widget radius(float topLeft,float topRight,float bottomRight,float bottomLeft){cornerProfile=CornerRadii.of(topLeft,topRight,bottomRight,bottomLeft,theme().cornerSmoothing);return this;} 025 public Widget radius(float all){return radius(all,all,all,all);} 026 public Widget corners(CornerRadii value){cornerProfile=value;return this;} 027 public CornerRadii corners(){return cornerProfile;} 028 public Widget corner(Corner corner,BezierCorner value){if(cornerProfile==null)cornerProfile=CornerRadii.uniform(theme().radius,theme().cornerSmoothing);cornerProfile=cornerProfile.with(corner,value);return this;} 029 public Widget text(String s){text=s==null?"":s;return this;} public String text(){return text;} 030 public Widget id(String s){id=s==null?"":s;return this;}public String id(){return id;} 031 public Widget tooltip(String s){tooltip=s==null?"":s;return this;}public String tooltip(){return tooltip;} 032 public Widget enabled(boolean value){enabled=value;return this;}public boolean enabled(){return enabled;} 033 public Widget visible(boolean value){visible=value;return this;}public boolean visible(){return visible;} 034 public Widget clipChildren(boolean b){clipChildren=b;return this;} 035 public Widget focusable(boolean b){focusable=b;return this;} 036 public Widget onClick(Consumer<Widget> listener){click=listener;return this;}public Widget onChange(Consumer<Widget> listener){change=listener;return this;} 037 public Widget context(PopupMenu menu){contextMenu=menu;return this;} 038 public float x(){return x;}public float y(){return y;}public float width(){return w;}public float height(){return h;} 039 public float absoluteX(){return ax;}public float absoluteY(){return ay;}public Rect rect(){return new Rect(ax,ay,w,h);} 040 public Widget parent(){return parent;}public Ui ui(){return ui;}public List<Widget> children(){return Collections.unmodifiableList(children);} 041 public boolean focused(){return ui!=null&&ui.focused()==this;} 042 public boolean contains(float px,float py){return visible&&rect().contains(px,py)&&clip.contains(px,py);} 043 public boolean canFocus(){return visible&&enabled&&focusable;} 044 public Widget add(Widget child){if(child==null)throw new NullPointerException("child");if(child==this)throw new IllegalArgumentException("Cannot parent itself");for(Widget a=this;a!=null;a=a.parent)if(a==child)throw new IllegalArgumentException("Widget cycle");if(child.parent!=null)child.parent.remove(child);children.add(child);child.parent=this;child.attach(ui);return this;} 045 public void remove(Widget child){if(children.remove(child)){child.parent=null;child.attach(null);}} 046 public void clear(){for(Widget c:children){c.parent=null;c.attach(null);}children.clear();} 047 public void attach(Ui context){if(ui!=null)detachedTheme=ui.theme();ui=context;for(Widget c:children)c.attach(context);} 048 public Widget find(String wanted){if(id.equals(wanted))return this;for(Widget c:children){Widget found=c.find(wanted);if(found!=null)return found;}return null;} 049 protected void layoutChildren(){} protected float offsetX(){return 0;}protected float offsetY(){return 0;} 050 protected Rect childClip(){return clipChildren?clip.intersect(rect()):clip;} 051 /** The same outline used by this widget's own glass surface, only for its descendants. */ 052 protected float contentClipRadius(){return theme().radius;} 053 protected final void pushContentClip(Painter p){p.pushShapeClip(rect(),contentClipRadius(),cornerProfile);} 054 protected final void popContentClip(Painter p){p.popShapeClip();} 055 public final void layout(float px,float py,Rect parentClip){ax=px+(smoothPosition?positionX.follow(x,clock(),75,theme().reducedMotion):x);ay=py+(smoothPosition?positionY.follow(y,clock(),75,theme().reducedMotion):y);clip=parentClip;layoutChildren();Rect cc=childClip();for(Widget c:children)c.layout(ax+offsetX(),ay+offsetY(),cc);} 056 public Widget hit(float px,float py){if(!visible||!enabled||!clip.contains(px,py))return null;for(int i=children.size()-1;i>=0;i--){Widget found=children.get(i).hit(px,py);if(found!=null)return found;}return rect().contains(px,py)?this:null;} 057 058 public final void draw(Painter original){ 059 if(!visible||clip.empty()||rect().intersect(clip).empty())return; 060 double now=clock();if(pendingPulse){pressUntil=Math.max(pressUntil,now+.085);pendingPulse=false;}float press=MotionValue.clamp(motion("press",visualDown||pressed||now<pressUntil?1:0,pressed?42:24,.69f)); 061 float hover=MotionValue.clamp(motion("hover",hovered?1:0,23,.99f)); 062 float scale=pressTransform()?1-.038f*motion("compression",visualDown||pressed||now<pressUntil?1:0,34,.62f):1; 063 if(pressTransform())scale+=hover*.003f; 064 float opacity=1,ty=0; 065 if(appearAt>=0&&!theme().reducedMotion){float age=(float)(now-appearAt);float progress=MotionValue.clamp(age/.24f);opacity=1-(float)Math.pow(1-progress,3);ty=(float)(12*Math.exp(-age*17));scale*=1-.012f*(1-opacity);if(age>.6)appearAt=-1;} 066 Painter p=Math.abs(scale-1)>.0001||opacity<.9999||ty>.01?new TransformPainter(original,ax+w/2,ay+h/2,scale,0,ty,opacity,clip):original; 067 p.clip(clip);p.corners(cornerProfile);paint(p); 068 boolean shaped=clipChildren&&!children.isEmpty(); 069 if(shaped)pushContentClip(p); 070 try{for(Widget c:children)c.draw(p);}finally{if(shaped)popContentClip(p);} 071 p.clip(clip);p.corners(cornerProfile);paintOverlay(p);p.corners(null); 072 // Focus is reflected by the material tint, never by an added outline. 073 } 074 public Widget appear(){appearAt=clock();return this;} 075 protected boolean pressTransform(){return false;} 076 protected double clock(){if(ui!=null)lastTime=ui.time();return lastTime;} 077 protected float motion(String key,float target){return motion(key,target,24,.86f);} 078 protected float motion(String key,float target,float frequency,float damping){MotionValue m=motions.get(key);if(m==null){m=new MotionValue();motions.put(key,m);}return m.to(target,clock(),frequency,damping,theme().reducedMotion);} 079 protected void snapMotion(String key,float value){MotionValue m=motions.get(key);if(m==null){m=new MotionValue();motions.put(key,m);}m.snap(value,clock());} 080 protected float follow(String key,float target,float response){MotionValue m=motions.get(key);if(m==null){m=new MotionValue();motions.put(key,m);}return m.follow(target,clock(),response,theme().reducedMotion);} 081 protected float hoverAmount(){return MotionValue.clamp(motion("hover",hovered?1:0,23,.99f));} 082 protected float pressAmount(){return MotionValue.clamp(motion("press",visualDown||pressed||clock()<pressUntil?1:0,visualDown?42:24,.69f));} 083 /** Called by Ui even for controls overriding event(), so animation is not optional by accident. */ 084 final void visualEvent(UiEvent e){if(e.type==UiEvent.Type.DOWN&&e.button==1){visualDown=true;pendingPulse=true;pressUntil=clock()+.08;primePress();}else if(e.type==UiEvent.Type.UP||e.type==UiEvent.Type.BLUR)visualDown=false;else if(e.type==UiEvent.Type.KEY&&(e.key==10||e.key==32)&&canFocus()){pendingPulse=true;pressUntil=clock()+.10;primePress();}} 085 private void primePress(){for(String key:new String[]{"press","compression"})if(!motions.containsKey(key)){MotionValue m=new MotionValue();m.snap(0,clock());motions.put(key,m);}} 086 protected void glassPart(Painter p,float xx,float yy,float ww,float hh,float radius,float tintExtra,int tintColor,float colorMix,float blur){Theme m=theme().copy();m.surface=Theme.mix(m.surface,tintColor,colorMix);m.tint=MotionValue.clamp(m.tint+tintExtra);m.surfaceOpacity*=enabled?1:.44f;p.glass(xx,yy,ww,hh,radius,m,blur);} 087 protected void stateSurface(Painter p,float selected,float prominence){Theme t=theme();float focus=(focused()&&ui!=null&&ui.keyboardMode())?motion("focus",1,23,.99f):motion("focus",0,23,.99f);glassPart(p,ax,ay,w,h,t.radius,t.hoverTint*hoverAmount()+t.pressTint*pressAmount()+t.selectedTint*selected+.075f*prominence+.06f*focus,t.accent,selected*.40f+prominence*.66f,t.blur);} 088 protected Theme theme(){return ui==null?(detachedTheme==null?Theme.dark():detachedTheme):ui.theme();} 089 protected int ink(){return enabled?theme().text:theme().disabledText;} 090 protected void paint(Painter p){}protected void paintOverlay(Painter p){} 091 protected void surface(Painter p){stateSurface(p,0,0);} 092 protected void label(Painter p,String s,float left,float top,float size,int color){p.text(s,ax+left,ay+top,size,color);} 093 protected void centered(Painter p,String s){float fs=theme().fontSize;p.text(s,ax+(w-p.textWidth(s,fs))/2,ay+(h-fs)/2,fs,ink());} 094 public void event(UiEvent e){if(!enabled||!visible)return;if(e.type==UiEvent.Type.DOWN&&e.button==1){pressed=true;}if(e.type==UiEvent.Type.UP&&e.button==1){boolean go=pressed&&contains(e.x,e.y);pressed=false;if(go)activate();}if(e.type==UiEvent.Type.KEY&&(e.key==10||e.key==32)&&canFocus()){activate();e.handled=true;}if(e.type==UiEvent.Type.BLUR)pressed=false;} 095 public void activate(){if(enabled&&click!=null)click.accept(this);} 096 public void changed(){if(change!=null)change.accept(this);} 097 public void hover(boolean value){hovered=value;}public boolean hovered(){return hovered;} 098 public void drawFocus(Painter p){}public void resetPress(){pressed=false;visualDown=false;pendingPulse=false;pressUntil=0;} 099 public PopupMenu contextMenu(){return contextMenu;} 100}