001package polaris.sdk.ui; 002import polaris.sdk.ui.controls.*; 003import polaris.sdk.ui.controls.TextField; 004import polaris.sdk.ui.render.*; 005import java.awt.*; 006import java.awt.event.*; 007import java.awt.im.*; 008import java.awt.font.TextHitInfo; 009import java.awt.image.BufferedImage; 010import java.io.*; 011import java.nio.ByteBuffer; 012import java.text.*; 013import java.util.concurrent.*; 014import java.util.function.*; 015import javax.imageio.ImageIO; 016/** Optional AWT/JAWT window host. All in-client widgets are rendered by OpenGL, not Swing. */ 017public final class DesktopWindow { 018 public static final class Options {public String title="LiquidGlass UI";public int width=1360,height=870,frames;public boolean animate=true,grid,light;public int maxFps=120;public String screenshot;} 019 public static final class Session {public final Ui ui;public final Canvas canvas;public final Frame window;public volatile boolean running=true;public volatile int frames;Session(Ui u,Canvas c,Frame w){ui=u;canvas=c;window=w;}} 020 private static volatile Session active;public static Session active(){return active;} 021 private static final class Ime {final String text;final int caret;final float x,y;Ime(String t,int c,float x,float y){text=t;caret=c;this.x=x;this.y=y;}} 022 private static final class Surface extends Canvas implements InputMethodRequests { 023 volatile Ime ime=new Ime("",0,0,0); 024 Surface(){setIgnoreRepaint(true);setFocusTraversalKeysEnabled(false);enableInputMethods(true);setBackground(new Color(28,28,28));} 025 @Override public void paint(Graphics g){}@Override public void update(Graphics g){} 026 @Override public InputMethodRequests getInputMethodRequests(){return this;} 027 @Override public Rectangle getTextLocation(TextHitInfo offset){Point pos;try{pos=getLocationOnScreen();}catch(IllegalComponentStateException e){pos=new Point();}return new Rectangle(pos.x+(int)ime.x,pos.y+(int)ime.y,1,22);} 028 @Override public TextHitInfo getLocationOffset(int x,int y){return null;} 029 @Override public int getInsertPositionOffset(){return ime.caret;} 030 @Override public AttributedCharacterIterator getCommittedText(int from,int to,AttributedCharacterIterator.Attribute[] attrs){String s=ime.text;int a=Math.max(0,Math.min(s.length(),from)),b=Math.max(a,Math.min(s.length(),to));return new AttributedString(s.substring(a,b)).getIterator();} 031 @Override public int getCommittedTextLength(){return ime.text.length();} 032 @Override public AttributedCharacterIterator cancelLatestCommittedText(AttributedCharacterIterator.Attribute[] attrs){return null;} 033 @Override public AttributedCharacterIterator getSelectedText(AttributedCharacterIterator.Attribute[] attrs){return null;} 034 } 035 public static void run(String title,int width,int height,Consumer<Ui> build)throws Exception{Options options=new Options();options.title=title;options.width=width;options.height=height;run(options,build);} 036 public static void run(Options options,Consumer<Ui> build)throws Exception{ 037 System.setProperty("sun.java2d.opengl","false");System.setProperty("sun.java2d.d3d","false"); 038 if(GraphicsEnvironment.isHeadless())throw new IllegalStateException("A graphical desktop / X11 session is required"); 039 final Frame[] frameHolder=new Frame[1];final Surface[] canvasHolder=new Surface[1];final ConcurrentLinkedQueue<UiEvent> events=new ConcurrentLinkedQueue<UiEvent>();final ConcurrentLinkedQueue<String> composition=new ConcurrentLinkedQueue<String>(); 040 final boolean[] closeRequested={false}; 041 EventQueue.invokeAndWait(()->{Frame window=new Frame(options.title);Surface canvas=new Surface();canvas.setPreferredSize(new Dimension(options.width,options.height));window.add(canvas);window.pack();window.setMinimumSize(new Dimension(980,660));window.setLocationRelativeTo(null);window.addWindowListener(new WindowAdapter(){@Override public void windowClosing(WindowEvent e){Session s=active;if(s!=null)s.running=false;else closeRequested[0]=true;}}); 042 MouseAdapter mouse=new MouseAdapter(){private UiEvent event(UiEvent.Type t,MouseEvent e){UiEvent u=new UiEvent(t).at(e.getX(),e.getY());u.button=e.getButton();u.modifiers=e.getModifiersEx();u.clicks=e.getClickCount();return u;} 043 @Override public void mouseMoved(MouseEvent e){events.add(event(UiEvent.Type.MOVE,e));}@Override public void mouseDragged(MouseEvent e){events.add(event(UiEvent.Type.MOVE,e));} 044 @Override public void mousePressed(MouseEvent e){canvas.requestFocusInWindow();events.add(event(UiEvent.Type.DOWN,e));}@Override public void mouseReleased(MouseEvent e){events.add(event(UiEvent.Type.UP,e));} 045 @Override public void mouseWheelMoved(MouseWheelEvent e){UiEvent u=event(UiEvent.Type.WHEEL,e);u.wheel=(float)e.getPreciseWheelRotation();events.add(u);} 046 };canvas.addMouseListener(mouse);canvas.addMouseMotionListener(mouse);canvas.addMouseWheelListener(mouse); 047 canvas.addKeyListener(new KeyAdapter(){@Override public void keyPressed(KeyEvent e){events.add(UiEvent.key(e.getKeyCode(),e.getModifiersEx()));e.consume();}@Override public void keyTyped(KeyEvent e){if(!e.isControlDown()&&!e.isMetaDown()&&!e.isAltDown()&&e.getKeyChar()>=32&&e.getKeyChar()!=127)events.add(UiEvent.text(String.valueOf(e.getKeyChar())));e.consume();}}); 048 canvas.addInputMethodListener(new InputMethodListener(){@Override public void inputMethodTextChanged(InputMethodEvent e){AttributedCharacterIterator it=e.getText();if(it!=null){StringBuilder b=new StringBuilder();for(char c=it.first();c!=AttributedCharacterIterator.DONE;c=it.next())b.append(c);int committed=Math.min(e.getCommittedCharacterCount(),b.length());if(committed>0)events.add(UiEvent.text(b.substring(0,committed)));composition.add(b.substring(committed));}else composition.add("");e.consume();}@Override public void caretPositionChanged(InputMethodEvent e){e.consume();}}); 049 canvas.addFocusListener(new FocusAdapter(){@Override public void focusLost(FocusEvent e){Session s=active;if(s!=null)s.ui.post(s.ui::clearInput);}});window.setVisible(true);canvas.requestFocusInWindow();frameHolder[0]=window;canvasHolder[0]=canvas; 050 }); 051 Surface canvas=canvasHolder[0];NativeBackend backend=null;NativePainter painter=null; 052 try{ 053 backend=NativeBackend.open(canvas);painter=new NativePainter(backend);Ui ui=new Ui();if(options.light)ui.theme(Theme.light());ui.background().grid(options.grid).animated(options.animate);Session session=new Session(ui,canvas,frameHolder[0]);session.running=!closeRequested[0];active=session;ui.resize(canvas.getWidth(),canvas.getHeight());build.accept(ui); 054 System.out.println("LiquidGlass UI 2.4.5 / Java "+System.getProperty("java.version"));System.out.println(backend.renderer()+" | "+backend.version());System.out.println("System font: "+painter.fontFamily()); 055 double start=System.nanoTime()/1e9,uiTime;float mx=0,my=0;while(session.running){long tick=System.nanoTime();uiTime=System.nanoTime()/1e9-start; 056 long size=backend.clientSize();int pw=(int)(size>>>32),ph=(int)size;float width=canvas.getWidth(),height=canvas.getHeight();if(pw>0&&ph>0&&width>0&&height>0){UiEvent event;while((event=events.poll())!=null){if(event.type==UiEvent.Type.MOVE||event.type==UiEvent.Type.DOWN||event.type==UiEvent.Type.UP){mx=event.x;my=event.y;}ui.input(event);}String c;while((c=composition.poll())!=null)ui.composition=c; 057 backend.background(pw,ph,ui.background().time(uiTime),ui.background().grid(),ui.theme()); 058 painter.begin(width,height,pw,ph);ui.render(painter,width,height,uiTime);painter.flushWindow(pw,ph,ui.theme().sigma,mx,my);backend.checkError(); 059 Widget focused=ui.focused();if(focused instanceof TextField){TextField text=(TextField)focused;String str=focused instanceof PasswordField?"":text.value();canvas.ime=new Ime(str,Math.min(str.length(),text.caret()),focused.absoluteX()+10,focused.absoluteY()+focused.height());}else canvas.ime=new Ime("",0,0,0); 060 session.frames++; 061 if(options.frames>0&&session.frames>=options.frames){if(options.screenshot!=null)saveImage(backend.screenshot(pw,ph),pw,ph,new File(options.screenshot));session.running=false;} 062 backend.swap(); 063 } 064 long sleep=Math.max(3_000_000L,(1_000_000_000L/Math.max(1,options.maxFps))-(System.nanoTime()-tick));if(session.running)Thread.sleep(sleep/1_000_000L,(int)(sleep%1_000_000L)); 065 } 066 System.out.println("PASS: "+session.frames+" rendered frames, GL_NO_ERROR; commands="+painter.commandCount()+", cached glyphs="+painter.glyphCount()); 067 }finally{try{if(painter!=null)painter.close();}finally{if(backend!=null)backend.close();active=null;EventQueue.invokeLater(()->frameHolder[0].dispose());}} 068 } 069 private static void saveImage(ByteBuffer bytes,int w,int h,File out)throws IOException{BufferedImage image=new BufferedImage(w,h,BufferedImage.TYPE_INT_ARGB);for(int y=0;y<h;y++)for(int x=0;x<w;x++){int i=((h-1-y)*w+x)*4;image.setRGB(x,y,((bytes.get(i+3)&255)<<24)|((bytes.get(i)&255)<<16)|((bytes.get(i+1)&255)<<8)|(bytes.get(i+2)&255));}if(out.getAbsoluteFile().getParentFile()!=null)out.getAbsoluteFile().getParentFile().mkdirs();ImageIO.write(image,"png",out);System.out.println("Screenshot: "+out.getAbsolutePath());} 070 private DesktopWindow(){} 071}