// Copyright 2000-2005 the Contributors, as shown in the revision logs. // Licensed under the GNU General Public License version 2 ("the License"). // You may not use this file except in compliance with the License. package org.ibex.core; // FIXME: are traps on x/y meaningful? // FIXME: trap on visible, trigger when parent visibility changes // FIXME: mouse move/release still needs to propagate to boxen in which the mouse was pressed and is still held down // FEATURE: reintroduce surface.abort // Broken: // - textures // - align/origin // - clipping (all forms) // - mouse events // - fonts/text // - vertical layout // - stroke clipping import java.util.*; import org.ibex.js.*; import org.ibex.util.*; import org.ibex.plat.*; import org.ibex.graphics.*; /** *

* Encapsulates the data for a single Ibex box as well as all layout * rendering logic. *

* *

The rendering process consists of four phases; each requires * one DFS pass over the tree

*
  1. pack(): each box sets its childrens' row/col *
    1. constrain(): contentwidth is computed *
    2. resize(): width/height and x/y positions are set *
    3. render(): children draw their content onto the PixelBuffer. *
    * * The first three passes together are called the reflow * phase. Reflowing is done in a seperate pass since SizeChanges * trigger a Surface.abort; if rendering were done in the same pass, * rendering work done prior to the Surface.abort would be wasted. */ public final class Box extends JS.Obj implements Callable, Mesh.Chain { public Mesh.Chain getMeshChainParent() { return parent; } public Mesh getMesh() { return mesh; } public Affine getAffine() { return transform; } // Macros ////////////////////////////////////////////////////////////////////// final void REPLACE() { for(Box b2 = this; b2 != null && !b2.test(REPLACE); b2 = b2.parent) b2.set(REPLACE); } final void RECONSTRAIN() { for(Box b2 = this; b2 != null && !b2.test(RECONSTRAIN); b2 = b2.parent) b2.set(RECONSTRAIN); } //#define CHECKSET_SHORT(prop) short nu = (short)JSU.toInt(value); if (nu == prop) break; prop = nu; //#define CHECKSET_INT(prop) int nu = JSU.toInt(value); if (nu == prop) break; prop = nu; //#define CHECKSET_FLAG(flag) boolean nu=JSU.toBoolean(value);if(nu == test(flag)) break; if (nu) set(flag); else clear(flag); //#define CHECKSET_BOOLEAN.N(prop) boolean nu = JSU.toBoolean(value); if (nu == prop) break; prop = nu; //#define CHECKSET_STRING(prop) if((value==null&&prop==null)||(value!=null&&JSU.toString(value).equals(prop)))break;prop=JSU.toString(value); // Instance Data ////////////////////////////////////////////////////////////////////// private Box parent = null; private Box redirect = this; private int flags = VISIBLE | RECONSTRAIN | REPLACE | STOP_UPWARD_PROPAGATION | CLIP | MOVED; private BalancedTree bt = null; private String text = null; private Font font = DEFAULT_FONT; private Picture texture = null; public int fillcolor = 0x00000000; private int strokecolor = 0xFF000000; public float flex = 1; private Path path = null; private Path clippath = null; private Affine transform = Affine.identity(); // FEATURE: polygon caching private Mesh polygon = null; private Mesh mesh = null; // specified directly by user public int minwidth = 0; public int maxwidth = Integer.MAX_VALUE; public int minheight = 0; public int maxheight = Integer.MAX_VALUE; // computed during reflow //public int width = 0; // AS MEASURED IN PARENT SPACE! //public int height = 0; // AS MEASURED IN PARENT SPACE! float _width; float _height; private int width; private int height; private int rootwidth; private int rootheight; public int getRootWidth() { return rootwidth; } public int getRootHeight() { return rootheight; } public int contentwidth = 0; // == max(minwidth, textwidth, sum(child.contentwidth)) public int contentheight = 0; // Instance Methods ///////////////////////////////////////////////////////////////////// /** invoked when a resource needed to render ourselves finishes loading */ public Object run(Object o) throws JSExn { if (texture == null) { Log.warn(Box.class, "perform() called with null texture"); return null; } if (texture.isLoaded) { setWidth(max(texture.width, minwidth), maxwidth); setHeight(max(texture.height, minheight), maxheight); dirty(); } else { JS res = texture.stream; texture = null; throw new JSExn("image not found: "+res.unclone()); } return null; } /** Adds the intersection of (x,y,w,h) and the node's current actual geometry to the Surface's dirty list */ public void dirty() { if (path==null) dirty(0, 0, contentwidth, contentheight); else dirty(path); } public void dirty(int x, int y, int w, int h) { } public void dirty(Path p) { Affine a = transform.copy(); for(Box cur = this; cur != null; cur = cur.parent) a.premultiply(cur.transform); long hbounds = p.horizontalBounds(a); long vbounds = p.verticalBounds(a); int x1 = (int)Encode.longToFloat2(hbounds); int x2 = (int)Encode.longToFloat1(hbounds); int y1 = (int)Encode.longToFloat2(vbounds); int y2 = (int)Encode.longToFloat1(vbounds); if (getSurface() != null) getSurface().dirty(x1, y1, x2-x1, y2-y1); } // Reflow //////////////////////////////////////////////////////////////////////////////////////// /** should only be invoked on the root box */ public void reflow() { constrain(this, Affine.identity(), new BoundingBox()); width = rootwidth = maxwidth; height = rootheight = maxheight; transform.e = 0; transform.f = 0; place(0, 0, width, height, false); } public float minWidth() { return Encode.longToFloat1(transform.rotateBox(minwidth, minheight)); } public float minHeight() { return Encode.longToFloat2(transform.rotateBox(minwidth, minheight)); } public float maxWidth() { return Encode.longToFloat1(transform.rotateBox(maxwidth, maxheight)); } public float maxHeight() { return Encode.longToFloat2(transform.rotateBox(maxwidth, maxheight)); } public float contentWidth() { return Encode.longToFloat1(transform.rotateBox(contentwidth, contentheight)); } public float contentHeight() { return Encode.longToFloat2(transform.rotateBox(contentwidth, contentheight)); } /** used (single-threadedly) in constrain() */ private static int xmin = 0, ymin = 0, xmax = 0, ymax = 0; private static class BoundingBox { public int xmin, ymin, xmax, ymax; public boolean unbounded() { return xmin==Integer.MAX_VALUE||xmax==Integer.MIN_VALUE||ymin==Integer.MAX_VALUE||ymax==Integer.MIN_VALUE; } public void reset() { xmin = Integer.MAX_VALUE; ymin = Integer.MAX_VALUE; xmax = Integer.MIN_VALUE; ymax = Integer.MIN_VALUE; } public void include(Affine a, float cw, float ch) { //#repeat contentwidth/contentheight contentheight/contentwidth minwidth/minheight row/col col/row \ // textwidth/textheight maxwidth/maxheight bounds/boundsy x1/y1 x2/y2 z1/q1 z2/q2 z3/q3 z4/q4 \ // horizontalBounds/verticalBounds e/f multiply_px/multiply_py xmin/ymin xmax/ymax float z1 = a.multiply_px(0, 0); float z2 = a.multiply_px(cw, ch); float z3 = a.multiply_px(cw, 0); float z4 = a.multiply_px(0, ch); xmin = min(xmin, (int)min(min(z1, z2), min(z3, z4))); xmax = max(xmax, (int)max(max(z1, z2), max(z3, z4))); //#end } public void include(Affine a, Path path) { //#repeat contentwidth/contentheight contentheight/contentwidth minwidth/minheight row/col col/row \ // textwidth/textheight maxwidth/maxheight bounds/boundsy x1/y1 x2/y2 z1/q1 z2/q2 z3/q3 z4/q4 \ // horizontalBounds/verticalBounds e/f multiply_px/multiply_py xmin/ymin xmax/ymax long bounds = path.horizontalBounds(a); float z1 = Encode.longToFloat2(bounds); float z2 = Encode.longToFloat1(bounds); float z3 = Encode.longToFloat2(bounds); float z4 = Encode.longToFloat1(bounds); xmin = min(xmin, (int)min(min(z1, z2), min(z3, z4))); xmax = max(xmax, (int)max(max(z1, z2), max(z3, z4))); //#end } } /** expand the {x,y}{min,max} boundingbox in space a to include this box */ public void constrain(Box b, Affine a, BoundingBox bbox) { contentwidth = 0; contentheight = 0; a = a.copy().premultiply(transform); BoundingBox bbox2 = new BoundingBox(); for(Box child = getChild(0); child != null; child = child.nextSibling()) { child.constrain(this, Affine.identity(), bbox2); if (bbox2.unbounded()) { /* FIXME: why? */ bbox2.reset(); continue; } if (packed()) { // packed boxes mush together their childrens' bounding boxes if (test(XAXIS)) { contentwidth = contentwidth + (bbox2.xmax-bbox2.xmin); contentheight = max(bbox2.ymax-bbox2.ymin, contentheight); } else { contentwidth = max(bbox2.xmax-bbox2.xmin, contentwidth); contentheight = contentheight + (bbox2.ymax-bbox2.ymin); } bbox2.reset(); } } if (!packed()) { // unpacked boxes simply use the "cumulative" bounding box contentwidth = bbox2.xmax-bbox2.xmin; contentheight = bbox2.ymax-bbox2.ymin; } contentwidth = bound(minwidth, contentwidth, maxwidth); contentheight = bound(minheight, contentheight, maxheight); bbox.include(a, contentwidth, contentheight); if (path!=null) bbox.include(a, path); } void place(float x, float y, float w, float h, boolean keep) { int oldw = width; int oldh = height; width = bound(contentwidth, (int)Encode.longToFloat1(transform.inverse().rotateBox(w, h)), test(HSHRINK)?contentwidth:maxwidth); height = bound(contentheight, (int)Encode.longToFloat2(transform.inverse().rotateBox(w, h)), test(VSHRINK)?contentheight:maxheight); if (oldw!=width || oldh!=height) mesh = null; if (!keep) { Affine a = transform; transform.e = 0; transform.f = 0; float e; float f; //#repeat e/f x/y multiply_px/multiply_py horizontalBounds/verticalBounds bounds/boundsy z1/z1y z2/z2y z3/z3y z4/z4y long bounds = path==null ? 0 : path.horizontalBounds(transform); float z1 = path==null ? a.multiply_px(0, 0) : Encode.longToFloat2(bounds); float z2 = path==null ? a.multiply_px(width, height) : Encode.longToFloat1(bounds); float z3 = path==null ? a.multiply_px(width, 0) : Encode.longToFloat2(bounds); float z4 = path==null ? a.multiply_px(0, height) : Encode.longToFloat1(bounds); e = (-1 * min(min(z1, z2), min(z3, z4))) + x; //#end transform.e = e; transform.f = f; } if (!packed()) { for(Box child = getChild(0); child != null; child = child.nextSibling()) child.place(0, 0, width, height, true); return; } float slack = test(XAXIS)?width:height, oldslack = 0, flex = 0, newflex = 0; for(Box child = getChild(0); child != null; child = child.nextSibling()) { if (!child.test(VISIBLE)) continue; if (test(XAXIS)) { child._width = child.contentWidth(); child._height = height; slack -= child._width; } else { child._height = child.contentHeight(); child._width = width; slack -= child._height; } flex += child.flex; } while(slack > 0 && flex > 0 && oldslack!=slack) { oldslack = slack; slack = test(XAXIS) ? width : height; newflex = 0; for(Box child = getChild(0); child != null; child = child.nextSibling()) { if (!child.test(VISIBLE)) continue; if (test(XAXIS)) { float oldwidth = child._width; if (child.test(HSHRINK)) child._width = min(child.maxWidth(), child._width+(oldslack*child.flex)/flex); slack -= child._width; if (child._width > oldwidth) newflex += child.flex; } else { float oldheight = child._height; if (child.test(VSHRINK)) child._height = min(child.maxHeight(), child._height+(oldslack*child.flex)/flex); slack -= child._height; if (child._height > oldheight) newflex += child.flex; } } flex = newflex; } float pos = slack / 2; for(Box child = getChild(0); child != null; child = child.nextSibling()) { if (!child.test(VISIBLE)) continue; if (test(XAXIS)) { child.place(pos, 0, child._width, child._height, false); pos += child._width; } else { child.place(0, pos, child._width, child._height, false); pos += child._height; } } } // Rendering Pipeline ///////////////////////////////////////////////////////////////////// private static final boolean OPTIMIZE = false; /** Renders self and children within the specified region. All rendering operations are clipped to xIn,yIn,wIn,hIn */ public void render(PixelBuffer buf, Affine a, Mesh clipFrom) { render(buf, a, clipFrom, Affine.identity(), 0); } public void render(PixelBuffer buf, Affine a, Mesh clipFrom, Affine clipa, int bg) { if (!test(VISIBLE)) return; a = a.copy().multiply(transform); clipa = clipa.copy().multiply(transform); if (mesh == null) if (path != null) mesh = new Mesh(path, true); else { if (((fillcolor & 0xFF000000) != 0x00000000 || parent == null) && (text==null||"".equals(text))) { mesh = new Mesh().addRect(0, 0, width, height); } // if (ret == 0) Platform.Scheduler.add(this); // FIXME: texture } if (mesh==null) { for(Box b = getChild(0); b != null; b = b.nextSibling()) b.render(buf, a, clipFrom, clipa, bg); if (!(text==null||text.equals(""))) font.rasterizeGlyphs(text, buf, a.copy(), clipFrom, clipa.copy(), strokecolor, 0); return; } if (clipFrom != null) clipFrom.subtract(mesh, clipa); Mesh mesh = treeSize() > 0 ? this.mesh.copy() : this.mesh; if ((fillcolor & 0xff000000)!=0) bg = fillcolor; for(Box b = getChild(0); b != null; b = b.nextSibling()) b.render(buf, a, mesh, Affine.identity(), bg); mesh.fill(buf, a, null, fillcolor, true); if ((strokecolor & 0xff000000) != 0) mesh.stroke(buf, a, strokecolor); if (!(text==null||text.equals(""))) font.rasterizeGlyphs(text, buf, a.copy(), clipFrom, clipa.copy(), strokecolor, bg); } // Methods to implement org.ibex.js.JS ////////////////////////////////////// public JS call(JS method, JS[] args) throws JSExn { switch (args.length) { case 1: { //#switch(JSU.toString(method)) case "indexof": Box b = (Box)args[0]; if (b.parent != this) return (redirect == null || redirect == this) ? JSU.N(-1) : redirect.call(method, args); return JSU.N(b.getIndexInParent()); case "distanceto": Box b = (Box)args[0]; JS ret = new JS.Obj(); ret.put(JSU.S("x"), JSU.N(b.localToGlobalX(0) - localToGlobalX(0))); ret.put(JSU.S("y"), JSU.N(b.localToGlobalY(0) - localToGlobalY(0))); return ret; //#end } } return super.call(method, args); } public JS get(JS name) throws JSExn { if (JSU.isInt(name)) return redirect == null ? null : redirect == this ? getChild(JSU.toInt(name)) : redirect.get(name); //#switch(JSU.toString(name)) case "surface": return parent == null ? null : parent.getAndTriggerTraps(name); case "indexof": return METHOD; case "distanceto": return METHOD; case "text": return JSU.S(text); case "path": { if (path != null) return JSU.S(path.toString()); if (text == null) return null; if (font == null) return null; String ret = ""; for(int i=0; i=0; i--) { Box child = cur.getChild(i); if (child == null) continue; // since this method is unsynchronized, we have to double-check globalx += child.transform.e; globaly += child.transform.f; if (child.test(VISIBLE) && child.inside(x - globalx, y - globaly)) { cur = child; continue OUTER; } globalx -= child.transform.e; globaly -= child.transform.f; } break; } return cur; } // Trivial Helper Methods (should be inlined) ///////////////////////////////////////// public final int fontSize() { return font == null ? DEFAULT_FONT.pointsize : font.pointsize; } public Enumeration jskeys() { throw new Error("you cannot apply for..in to a " + this.getClass().getName()); } public Box getRoot() { return parent == null ? this : parent.getRoot(); } public Surface getSurface() { return Surface.fromBox(getRoot()); } private final boolean packed() { return test(YAXIS) || test(XAXIS); } Box nextPackedSibling() { Box b = nextSibling(); return b == null || (b.packed() && b.test(VISIBLE))?b:b.nextPackedSibling(); } Box firstPackedChild() { Box b = getChild(0); return b == null || (b.packed() && b.test(VISIBLE))?b:b.nextPackedSibling(); } public float globalToLocalX(float x) { return parent == null ? x : parent.globalToLocalX(x - transform.e); } public float globalToLocalY(float y) { return parent == null ? y : parent.globalToLocalY(y - transform.f); } public float localToGlobalX(float x) { return parent == null ? x : parent.globalToLocalX(x + transform.e); } public float localToGlobalY(float y) { return parent == null ? y : parent.globalToLocalY(y + transform.f); } static short min(short a, short b) { if (ab) return a; else return b; } static int max(int a, int b) { if (a>b) return a; else return b; } static float max(float a, float b) { if (a>b) return a; else return b; } static int min(int a, int b, int c) { if (a<=b && a<=c) return a; else if (b<=c && b<=a) return b; else return c; } static int max(int a, int b, int c) { if (a>=b && a>=c) return a; else if (b>=c && b>=a) return b; else return c; } static int bound(int a, int b, int c) { if (c < b) return c; if (a > b) return a; return b; } final boolean inside(int x, int y) { return test(VISIBLE) && x >= 0 && y >= 0 && x < width && y < height; } void set(int mask) { flags |= mask; } void set(int mask, boolean setclear) { if (setclear) set(mask); else clear(mask); } void clear(int mask) { flags &= ~mask; } public boolean test(int mask) { return ((flags & mask) == mask); } // Tree Handling ////////////////////////////////////////////////////////////////////// public final int getIndexInParent() { return parent == null ? 0 : parent.indexNode(this); } public final Box nextSibling() { return parent == null ? null : parent.getChild(parent.indexNode(this) + 1); } public final Box prevSibling() { return parent == null ? null : parent.getChild(parent.indexNode(this) - 1); } public final Box getChild(int i) { if (i < 0) return null; if (i >= treeSize()) return null; return (Box)getNode(i); } // Tree Manipulation ///////////////////////////////////////////////////////////////////// public void removeSelf() { if (parent != null) { parent.removeChild(parent.indexNode(this)); return; } Surface surface = Surface.fromBox(this); if (surface != null) surface.dispose(true); } /** remove the i^th child */ public void removeChild(int i) { Box b = getChild(i); b.RECONSTRAIN(); b.dirty(); b.clear(MOUSEINSIDE); deleteNode(i); b.parent = null; RECONSTRAIN(); putAndTriggerTrapsAndCatchExceptions(JSU.S("ChildChange"), b); } public void put(int i, JS value) throws JSExn { if (i < 0) return; if (value != null && !(value instanceof Box)) { if (Log.on) JSU.warn("attempt to set a numerical property on a box to a non-box"); return; } if (redirect == null) { if (value == null) putAndTriggerTrapsAndCatchExceptions(JSU.S("ChildChange"), getChild(i)); else JSU.warn("attempt to add/remove children to/from a node with a null redirect"); } else if (redirect != this) { if (value != null) putAndTriggerTrapsAndCatchExceptions(JSU.S("ChildChange"), value); redirect.put(i, value); if (value == null) { Box b = (Box)redirect.get(JSU.N(i)); if (b != null) putAndTriggerTrapsAndCatchExceptions(JSU.S("ChildChange"), b); } } else if (value == null) { if (i < 0 || i > treeSize()) return; Box b = getChild(i); removeChild(i); putAndTriggerTrapsAndCatchExceptions(JSU.S("ChildChange"), b); } else { Box b = (Box)value; // check if box being moved is currently target of a redirect for(Box cur = b.parent; cur != null; cur = cur.parent) if (cur.redirect == b) { if (Log.on) JSU.warn("attempt to move a box that is the target of a redirect"); return; } // check for recursive ancestor violation for(Box cur = this; cur != null; cur = cur.parent) if (cur == b) { if (Log.on) JSU.warn("attempt to make a node a parent of its own ancestor"); if (Log.on) Log.info(this, "box == " + this + " ancestor == " + b); return; } if (b.parent != null) b.parent.removeChild(b.parent.indexNode(b)); insertNode(i, b); b.parent = this; // need both of these in case child was already uncalc'ed b.RECONSTRAIN(); RECONSTRAIN(); b.dirty(); putAndTriggerTrapsAndCatchExceptions(JSU.S("ChildChange"), b); } } public void putAndTriggerTrapsAndCatchExceptions(JS name, JS val) { try { putAndTriggerTraps(name, val); } catch (JSExn e) { JSU.log("caught js exception while putting to trap \""+ JSU.str(name)+"\""); JSU.log(e); } catch (Exception e) { JSU.log("caught exception while putting to trap \""+ JSU.str(name)+"\""); JSU.log(e); } } // BalancedTree functions private void insertNode(int p, Box b) { if(bt == null) bt = new BalancedTree(); bt.insertNode(p,b); } private int treeSize() { return bt == null ? 0 : bt.treeSize(); } private int indexNode(Box b) { return bt == null ? -1 : bt.indexNode(b); } private void deleteNode(int p) { bt.deleteNode(p); } private Box getNode(int p) { return (Box)bt.getNode(p); } // FIXME memory leak final static JS.Method METHOD = new JS.Method(); final static Basket.Map boxToCursor = new Basket.Hash(500, 3); final static JS SIZECHANGE = JSU.S("SizeChange"); final static Font DEFAULT_FONT = Font.getFont(Main.vera, 10); // Flags ////////////////////////////////////////////////////////////////////// public static final int MOUSEINSIDE = 0x00000001; public static final int XAXIS = 0x00000002; public static final int YAXIS = 0x00000004; public static final int VISIBLE = 0x00000008; public static final int VSHRINK = 0x00000010; public static final int HSHRINK = 0x00000020; public static final int RECONSTRAIN = 0x00000040; public static final int REPLACE = 0x00000080; // if true, this box has cursor in the cursor hash; FEATURE: GC issues? public static final int CURSOR = 0x00010000; public static final int CLIP = 0x00020000; public static final int STOP_UPWARD_PROPAGATION = 0x00040000; public static final int MOVED = 0x00080000; }