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}