001package polaris.sdk.ui.render;
002import polaris.sdk.ui.*;
003import java.nio.*;
004/** Collects clipped drawing commands and submits them with one JNI call per UI frame. */
005public final class NativePainter implements Painter,AutoCloseable {
006 public static final int STRIDE=64;private final NativeBackend backend;private final GlyphAtlas atlas;
007 private FloatBuffer commands=allocate(8192);private int count;private Rect clip=new Rect(0,0,1,1);private float uiWidth,uiHeight;private boolean closed;private Theme settings=Theme.dark();private CornerRadii cornerProfile;private float dpiX=1,dpiY=1;private int shapeDepth;
008 public NativePainter(NativeBackend backend){if(backend==null)throw new NullPointerException("backend");this.backend=backend;backend.check();atlas=new GlyphAtlas(backend);}
009 private static FloatBuffer allocate(int count){return ByteBuffer.allocateDirect(count*STRIDE*4).order(ByteOrder.nativeOrder()).asFloatBuffer();}
010 /** Supply physical dimensions before text layout to avoid a first-frame DPI mismatch. */
011 public void begin(float width,float height,int pixelWidth,int pixelHeight){
012  if(pixelWidth<=0||pixelHeight<=0||width<=0||height<=0)throw new IllegalArgumentException("viewport");
013  dpiX=pixelWidth/width;dpiY=pixelHeight/height;begin(width,height);
014 }
015 @Override public void configure(Theme theme){backend.check();if(closed)throw new IllegalStateException("Painter closed");if(theme==null)throw new NullPointerException("theme");if(theme.antialiasing==null||theme.textMode==null)throw new IllegalArgumentException("Missing render quality/text mode");if(!Float.isFinite(theme.textContrast)||theme.textContrast<.5f||theme.textContrast>2||!Float.isFinite(theme.cornerSmoothing)||theme.cornerSmoothing<0||theme.cornerSmoothing>1||!Float.isFinite(theme.letterSpacing))throw new IllegalArgumentException("Invalid global render settings");settings=theme;atlas.configure(theme);}
016 @Override public void corners(CornerRadii profile){cornerProfile=profile;}
017 public void begin(float width,float height){backend.check();if(closed)throw new IllegalStateException("Painter closed");if(width<=0||height<=0)throw new IllegalArgumentException("viewport");count=0;shapeDepth=0;commands.clear();cornerProfile=null;uiWidth=width;uiHeight=height;clip=new Rect(0,0,width,height);}
018 public void flush(int framebuffer,int colorAttachment,int pw,int ph,float sigma,float mx,float my){if(shapeDepth!=0)throw new IllegalStateException("Unbalanced shaped clips: "+shapeDepth);if(pw<=0||ph<=0)return;dpiX=pw/uiWidth;dpiY=ph/uiHeight;commands.position(0);backend.submit(commands,count,framebuffer,colorAttachment,pw,ph,uiWidth,uiHeight,sigma,mx,my);}
019 public void flushWindow(int width,int height,float sigma,float mx,float my){flush(0,0x0405,width,height,sigma,mx,my);}
020 public int commandCount(){return count;}public int glyphCount(){return atlas.size();}public int sdfGlyphCount(){return atlas.sdfCount();}public String fontFamily(){return atlas.family();}
021 @Override public void clip(Rect r){if(r==null)throw new NullPointerException("clip");clip=r;}
022 @Override public void pushShapeClip(Rect bounds,float radius,CornerRadii profile){
023  if(bounds==null)throw new NullPointerException("bounds");
024  if(!Float.isFinite(bounds.x)||!Float.isFinite(bounds.y)||!Float.isFinite(bounds.w)||!Float.isFinite(bounds.h)||!Float.isFinite(radius)||bounds.w<0||bounds.h<0)throw new IllegalArgumentException("Invalid shaped clip");
025  if(shapeDepth>=32)throw new IllegalStateException("Maximum shaped-clip nesting is 32");
026  int off=command(10,bounds.x,bounds.y,bounds.w,bounds.h,0xffffffff);
027  CornerRadii previous=cornerProfile;cornerProfile=profile;
028  try{commands.put(off+5,radius);rounding(off,radius,settings);}finally{cornerProfile=previous;}
029  shapeDepth++;
030 }
031 @Override public void popShapeClip(){if(shapeDepth<=0)throw new IllegalStateException("Shaped clip stack underflow");command(11,0,0,0,0,0xffffffff);shapeDepth--;}
032 private int command(int kind,float x,float y,float w,float h,int color){boolean stateCommand=kind==9||kind==10||kind==11;if(!stateCommand&&(clip.empty()||(color>>>24)==0))return -1;if(!stateCommand&&kind!=4&&new Rect(x,y,w,h).intersect(clip).empty())return -1;
033  if(count>=65536)throw new IllegalStateException("UI draw command limit exceeded (65,536)");if((count+1)*STRIDE>commands.capacity()){FloatBuffer larger=allocate(Math.min(65536,commands.capacity()/STRIDE*2));commands.position(0);larger.put(commands);commands=larger;}
034  int off=(count++)*STRIDE;for(int i=0;i<STRIDE;i++)commands.put(off+i,0);commands.put(off,kind);commands.put(off+1,x);commands.put(off+2,y);commands.put(off+3,w);commands.put(off+4,h);commands.put(off+6,((color>>16)&255)/255f);commands.put(off+7,((color>>8)&255)/255f);commands.put(off+8,(color&255)/255f);commands.put(off+9,(color>>>24)/255f);commands.put(off+18,clip.x);commands.put(off+19,clip.y);commands.put(off+20,clip.w);commands.put(off+21,clip.h);commands.put(off+27,settings.antialiasing.ordinal());commands.put(off+45,settings.textContrast);return off;
035 }
036 @Override public void glass(float x,float y,float w,float h,float radius,Theme t,float mix){int off=command(0,x,y,w,h,(Math.round(MotionValue.clamp(t.surfaceOpacity)*255)<<24)|(t.surface&0xffffff));if(off>=0){commands.put(off+5,radius);commands.put(off+22,mix);commands.put(off+23,t.refraction);commands.put(off+24,t.shine);commands.put(off+25,t.tint);commands.put(off+26,t.dispersion);commands.put(off+27,t.antialiasing.ordinal());rounding(off,radius,t);color(off+36,t.highlight);color(off+40,t.shadow);}}
037 @Override public void fill(float x,float y,float w,float h,float radius,int color){if(w<=0||h<=0)return;int off=command(1,x,y,w,h,color);if(off>=0){commands.put(off+5,radius);rounding(off,radius,settings);}}
038 @Override public void stroke(float x,float y,float w,float h,float radius,float thickness,int color){if(w<=0||h<=0)return;int off=command(1,x,y,w,h,color);if(off>=0){commands.put(off+5,radius);commands.put(off+10,thickness);rounding(off,radius,settings);}}
039 @Override public void line(float x,float y,float x2,float y2,float thickness,int color){int off=command(4,x,y,x2-x,y2-y,color);if(off>=0)commands.put(off+10,thickness);}
040 @Override public void ring(float x,float y,float size,float thickness,float progress,float start,int color){int off=command(3,x,y,size,size,color);if(off>=0){commands.put(off+10,thickness);commands.put(off+11,progress);commands.put(off+12,start);}}
041 @Override public void colorPlane(float x,float y,float w,float h,float hue){colorPlaneAlpha(x,y,w,h,hue,1);}
042 @Override public void colorPlaneAlpha(float x,float y,float w,float h,float hue,float alpha){int off=command(5,x,y,w,h,Theme.alpha(0xffffffff,alpha));if(off>=0)commands.put(off+11,hue);}
043
044 @Override public void text(String text,float x,float y,float size,int color){textWeightScaled(text,x,y,size,color,400,1);}
045 @Override public void textScaled(String text,float x,float y,float size,int color,float scale){textWeightScaled(text,x,y,size,color,400,scale);}
046 @Override public void textWeight(String text,float x,float y,float size,int color,int weight){textWeightScaled(text,x,y,size,color,weight,1);}
047 private void color(int off,int c){commands.put(off,((c>>16)&255)/255f);commands.put(off+1,((c>>8)&255)/255f);commands.put(off+2,(c&255)/255f);commands.put(off+3,(c>>>24)/255f);}
048 private void rounding(int off,float radius,Theme theme){
049  float minSize=Math.min(commands.get(off+3),commands.get(off+4));
050  // Circular thumbs/avatars and capsules remain circular unless explicitly overridden.
051  boolean cubic=theme.bezierCorners&&radius>0&&radius<minSize*.5f-.001f;
052  if(cornerProfile!=null){
053   for(Corner corner:Corner.values()){BezierCorner q=cornerProfile.get(corner);commands.put(off+28+corner.ordinal(),q.radius);commands.put(off+32+corner.ordinal(),q.handle());}
054   cubic=true;
055  }else{float r=radius<0?minSize/2:radius;for(int i=0;i<4;i++){commands.put(off+28+i,r);commands.put(off+32+i,.55228475f+.44771525f*theme.cornerSmoothing);}}
056  commands.put(off+44,cubic?1:0);
057 }
058 private GlyphAtlas.Glyph layoutGlyph(int cp,float size,int weight){
059  boolean sdf=settings.textMode==TextMode.SDF||settings.textMode==TextMode.AUTO&&size*dpiY>24;
060  return atlas.get(cp,size,weight,dpiY,sdf,settings.antialiasing!=Antialiasing.OFF);
061 }
062 @Override public void textWeightScaled(String text,float x,float y,float size,int color,int weight,float scale){
063  if(text==null||text.isEmpty()||size<=0||scale<=0)return;
064  // Snap the run origin/baseline first so fractional advances do not make different letters
065  // jump at different pointer subpixels. Animation uses continuous SDF positions instead.
066  if(settings.textPixelSnap && Math.abs(scale-1)<.001f && (settings.textMode==TextMode.COVERAGE || settings.textMode==TextMode.AUTO&&size*dpiY<=24)){
067   x=Math.round(x*dpiX)/dpiX;y=Math.round((y+size*.84f)*dpiY)/dpiY-size*.84f;
068  }
069  float xx=x,yy=y;
070  boolean moving=Math.abs(scale-1)>.018f;
071  for(int i=0;i<text.length();){int cp=text.codePointAt(i);i+=Character.charCount(cp);if(cp=='\n'){xx=x;yy+=(size+5)*scale;continue;}
072   GlyphAtlas.Glyph layout=layoutGlyph(cp,size,weight),glyph=layout;
073   if(settings.textMode==TextMode.AUTO){
074    // Warm the alternate cache once, so press/release never rasterizes at each animation scale.
075    GlyphAtlas.Glyph scalable=atlas.get(cp,size,weight,dpiY,true,true);
076    if(moving||size*dpiY>24)glyph=scalable;
077   }
078   float gw=glyph.w/glyph.emPixels*size*scale,gh=glyph.h/glyph.emPixels*size*scale;
079   float gx=xx+glyph.offsetXEm*size*scale,gy=yy+(size*.84f+glyph.offsetYEm*size)*scale;
080   if(settings.textPixelSnap&&!glyph.sdf&&Math.abs(scale-1)<.001f){gx=Math.round(gx*dpiX)/dpiX;gy=Math.round(gy*dpiY)/dpiY;}
081   int off=command(2,gx,gy,gw,gh,color);
082   if(off>=0){
083    commands.put(off+11,glyph.sdf?1:0);commands.put(off+12,glyph.range);
084    commands.put(off+13,glyph.x/(float)GlyphAtlas.SIZE);commands.put(off+14,glyph.y/(float)GlyphAtlas.SIZE);
085    commands.put(off+15,glyph.w/(float)GlyphAtlas.SIZE);commands.put(off+16,glyph.h/(float)GlyphAtlas.SIZE);
086    commands.put(off+17,Float.intBitsToFloat(glyph.texture));
087   }
088   xx+=(layout.advanceEm*size+(i<text.length()&&text.charAt(i)!='\n'?settings.letterSpacing:0))*scale;
089  }
090 }
091 @Override public float textWidth(String text,float size){return textWidth(text,size,400);}
092 @Override public float textWidth(String text,float size,int weight){
093  if(text==null||text.isEmpty())return 0;float current=0,longest=0;
094  for(int i=0;i<text.length();){int cp=text.codePointAt(i);i+=Character.charCount(cp);if(cp=='\n'){longest=Math.max(longest,current);current=0;}else current+=layoutGlyph(cp,size,weight).advanceEm*size+(i<text.length()&&text.charAt(i)!='\n'?settings.letterSpacing:0);}
095  return Math.max(longest,current);
096 }
097 @Override public void backdrop(){command(9,0,0,0,0,0xffffffff);}
098 @Override public void close(){if(closed)return;atlas.close();closed=true;}
099}