diff --git a/TotalCrossSDK/src/main/java/totalcross/ui/Button.java b/TotalCrossSDK/src/main/java/totalcross/ui/Button.java
index d3fda61a6..b1abd59f1 100644
--- a/TotalCrossSDK/src/main/java/totalcross/ui/Button.java
+++ b/TotalCrossSDK/src/main/java/totalcross/ui/Button.java
@@ -788,97 +788,102 @@ public void onPaint(Graphics g) {
if (skipPaint) {
return;
}
-
+
int tx = tx0;
int ty = ty0;
int ix = ix0;
int iy = iy0;
-
- if ((uiMaterial || uiAndroid) && this.border == BORDER_3D) {
- int bbackColor = isEnabled() ?
- backColor : (uiMaterial ? disabledColor : Color.interpolate(parent.backColor, backColor));
- if(!transparentBackground) {
- g.backColor = bbackColor;
- if(!drawNinePatch) {
- g.fillRect(4, 4, width - 6, height - 6);
+
+ boolean rendererPainted = controlRenderer != null;
+ if (rendererPainted) {
+ controlRenderer.draw(g, 0, 0, width, height, armed);
+ } else {
+ if ((uiMaterial || uiAndroid) && this.border == BORDER_3D) {
+ int bbackColor = isEnabled() ?
+ backColor : (uiMaterial ? disabledColor : Color.interpolate(parent.backColor, backColor));
+ if(!transparentBackground) {
+ g.backColor = bbackColor;
+ if(!drawNinePatch) {
+ g.fillRect(4, 4, width - 6, height - 6);
+ }
}
- }
- if(npParts != null) {
- try {
- if (!drawTranslucentBackground(g, armed ? alphaValue >= 80 ? alphaValue / 2 : alphaValue * 2 : alphaValue)) {
- if(npback == null) {
- npback = NinePatch.getInstance().getNormalInstance(npParts, width, height,
- isEnabled() ? transparentBackground ? borderColor : bbackColor : bbackColor, false);
+ if(npParts != null) {
+ try {
+ if (!drawTranslucentBackground(g, armed ? alphaValue >= 80 ? alphaValue / 2 : alphaValue * 2 : alphaValue)) {
+ if(npback == null) {
+ npback = NinePatch.getInstance().getNormalInstance(npParts, width, height,
+ isEnabled() ? transparentBackground ? borderColor : bbackColor : bbackColor, false);
+ }
+ this.originalForeColor = this.originalForeColor == -1 ? this.foreColor : this.originalForeColor;
+ this.foreColor = this.isEnabled() ? this.originalForeColor : Color.BLACK;
}
- this.originalForeColor = this.originalForeColor == -1 ? this.foreColor : this.originalForeColor;
- this.foreColor = this.isEnabled() ? this.originalForeColor : Color.BLACK;
- }
- if (npback != null) {
- NinePatch.tryDrawImage(g, armed && (isSticky || effect == null) ?
- NinePatch.getInstance().getPressedInstance(npback, backColor,pressColor) : npback,
- 0, 0);
+ if (npback != null) {
+ NinePatch.tryDrawImage(g, armed && (isSticky || effect == null) ?
+ NinePatch.getInstance().getPressedInstance(npback, backColor,pressColor) : npback,
+ 0, 0);
+ }
+ } catch (ImageException ie) {
+ ie.printStackTrace();
}
- } catch (ImageException ie) {
- ie.printStackTrace();
}
- }
- } else {
- boolean isBorderRound = border == BORDER_ROUND;
- if (uiMaterial && effect != null) {
- effect.darkSideOnPress = border != BORDER_NONE;
- }
- if (isAndroidStyle) {
- if (translucentShape == TranslucentShape.NONE && !uiMaterial && !Settings.isOpenGL) {
- g.getClip(clip);
- g.backColor = Settings.isOpenGL ? parent.backColor : g.getPixel(clip.x, clip.y);
- g.fillRect(0, 0, width, height);
+ } else {
+ boolean isBorderRound = border == BORDER_ROUND;
+ if (uiMaterial && effect != null) {
+ effect.darkSideOnPress = border != BORDER_NONE;
}
- } else if (!isBorderRound && (!transparentBackground || (armed && fillWhenPressedOnTransparentBackground)
- || drawBordersIfTransparentBackground)) {
- paintBackground(g);
- }
-
- if (isBorderRound) {
- g.backColor = backColor;
- g.fillRoundRect(0, 0, width, height,
+ if (isAndroidStyle) {
+ if (translucentShape == TranslucentShape.NONE && !uiMaterial && !Settings.isOpenGL) {
+ g.getClip(clip);
+ g.backColor = Settings.isOpenGL ? parent.backColor : g.getPixel(clip.x, clip.y);
+ g.fillRect(0, 0, width, height);
+ }
+ } else if (!isBorderRound && (!transparentBackground || (armed && fillWhenPressedOnTransparentBackground)
+ || drawBordersIfTransparentBackground)) {
+ paintBackground(g);
+ }
+
+ if (isBorderRound) {
+ g.backColor = backColor;
+ g.fillRoundRect(0, 0, width, height,
+ uiMaterial ? UnitsConverter.toPixels(DP + 4) : height / roundBorderFactor);
+ } else if (this.border == BORDER_OUTLINED) {
+ g.foreColor = transparentBackground ? this.borderColor : this.backColor;
+ g.drawRoundRect(0, 0, width, height,
uiMaterial ? UnitsConverter.toPixels(DP + 4) : height / roundBorderFactor);
- } else if (this.border == BORDER_OUTLINED) {
- g.foreColor = transparentBackground ? this.borderColor : this.backColor;
- g.drawRoundRect(0, 0, width, height,
- uiMaterial ? UnitsConverter.toPixels(DP + 4) : height / roundBorderFactor);
-
- } else if (isAndroidStyle || (uiMaterial && border != BORDER_NONE)) {
- paintImage(g, true, 0, 0);
- }
+
+ } else if (isAndroidStyle || (uiMaterial && border != BORDER_NONE)) {
+ paintImage(g, true, 0, 0);
+ }
- int border = txtPos == CENTER ? 0 : Math.min(2, this.border); // guich@tc112_31
- g.setClip(border, border, width - (border << 1), height - (border << 1)); // guich@101: cut text if button is
- // too small - guich@510_4
-
- boolean isBorder3D = border == BORDER_3D_HORIZONTAL_GRADIENT || border == BORDER_3D_VERTICAL_GRADIENT;
- if (armed && !isAndroidStyle && shiftOnPress && (isBorder3D || uiVista || (img != null && text == null))) { // guich@tc100: if this is an image-only button, let the button be pressed
- int inc = isBorder3D ? borderWidth3DG : 1;
- tx += inc;
- ix += inc;
- ty += inc;
- iy += inc;
- }
- if(this.border != BORDER_OUTLINED)
- {
- g.foreColor = fColor;
- }
- }
+ int border = txtPos == CENTER ? 0 : Math.min(2, this.border); // guich@tc112_31
+ g.setClip(border, border, width - (border << 1), height - (border << 1)); // guich@101: cut text if button is
+ // too small - guich@510_4
- if (getDoEffect() && effect != null) {
- if(this.border == BORDER_OUTLINED) {
- effect.color = Color.getBrightness(fColor) < 127 ? Color.brighter(fColor, 64) : Color.darker(fColor, 64);
- int backup = this.backColor;
- this.backColor = this.foreColor;
- effect.paintEffect(g);
- this.backColor = backup;
- } else {
- effect.paintEffect(g);
+ boolean isBorder3D = border == BORDER_3D_HORIZONTAL_GRADIENT || border == BORDER_3D_VERTICAL_GRADIENT;
+ if (armed && !isAndroidStyle && shiftOnPress && (isBorder3D || uiVista || (img != null && text == null))) { // guich@tc100: if this is an image-only button, let the button be pressed
+ int inc = isBorder3D ? borderWidth3DG : 1;
+ tx += inc;
+ ix += inc;
+ ty += inc;
+ iy += inc;
+ }
+ if(this.border != BORDER_OUTLINED)
+ {
+ g.foreColor = fColor;
+ }
+ }
+
+ if (getDoEffect() && effect != null) {
+ if(this.border == BORDER_OUTLINED) {
+ effect.color = Color.getBrightness(fColor) < 127 ? Color.brighter(fColor, 64) : Color.darker(fColor, 64);
+ int backup = this.backColor;
+ this.backColor = this.foreColor;
+ effect.paintEffect(g);
+ this.backColor = backup;
+ } else {
+ effect.paintEffect(g);
+ }
}
}
diff --git a/TotalCrossSDK/src/main/java/totalcross/ui/Container.java b/TotalCrossSDK/src/main/java/totalcross/ui/Container.java
index 03eca01e9..f2397e175 100644
--- a/TotalCrossSDK/src/main/java/totalcross/ui/Container.java
+++ b/TotalCrossSDK/src/main/java/totalcross/ui/Container.java
@@ -15,6 +15,7 @@
import totalcross.ui.event.PenEvent;
import totalcross.ui.event.PenListener;
import totalcross.ui.gfx.Graphics;
+import totalcross.ui.gfx.RRect;
import totalcross.ui.gfx.Rect;
import totalcross.ui.image.Image;
import totalcross.ui.image.ImageException;
@@ -115,6 +116,8 @@ public class Container extends Control {
* @see #setInsets
*/
protected Insets insets = new Insets(); // guich@tc110_87
+ protected Insets userInsets = new Insets();
+ protected RRect childrenClip;
/** Set to true to always erase the background when repainting this container.
* @since TotalCross 1.0
@@ -196,11 +199,11 @@ public boolean isPressed() {
*/
public void setInsets(int left, int right, int top, int bottom) // guich@tc110_87
{
- int gap = borderStyle == BORDER_NONE || borderStyle == BORDER_TOP ? 0 : borderStyle == BORDER_SIMPLE ? 1 : 2;
- insets.left = left + gap;
- insets.right = right + gap;
- insets.top = top + gap;
- insets.bottom = bottom + gap;
+ userInsets.left = left;
+ userInsets.right = right;
+ userInsets.top = top;
+ userInsets.bottom = bottom;
+ recomputeInsets();
}
/** Copy the current insets values into the given insets. If you call this method often,
@@ -504,22 +507,44 @@ public void broadcastEvent(Event e) {
/** Called by the system to draw the children of the container. */
public void paintChildren() {
- for (Control child = children; child != null; child = child.next) {
- if (child.visible) // guich@200: ignore hidden controls - note: a window added to a container may not be painted correctly
- {
- if (child.offscreen != null) {
- Graphics g = getGraphics();
- g.drawImage(child.offscreen, child.x, child.y);
- if (child.offscreen0 != null) {
- g.drawImage(child.offscreen0, child.x, child.y);
- }
- } else {
- child.onPaint(child.getGraphics());
- if (child.asContainer != null) {
- child.asContainer.paintChildren();
+ paintChildren(getGraphics());
+ }
+
+ private void paintChildren(Graphics g) {
+ if (g == null) {
+ return;
+ }
+
+ RRect oldClip = null;
+ if (childrenClip != null) {
+ oldClip = g.getClip();
+ if (childrenClip.width <= 0 || childrenClip.height <= 0) {
+ return;
+ }
+ g.setClip(childrenClip);
+ }
+ try {
+ for (Control child = children; child != null; child = child.next) {
+ if (child.visible) // guich@200: ignore hidden controls - note: a window added to a container may not be painted correctly
+ {
+ if (child.offscreen != null) {
+ g.drawImage(child.offscreen, child.x, child.y);
+ if (child.offscreen0 != null) {
+ g.drawImage(child.offscreen0, child.x, child.y);
+ }
+ } else {
+ Graphics childGraphics = child.getGraphics(g);
+ child.onPaint(childGraphics);
+ if (child.asContainer != null) {
+ child.asContainer.paintChildren(childGraphics);
+ }
}
}
}
+ } finally {
+ if (oldClip != null) {
+ g.setClip(oldClip);
+ }
}
}
@@ -534,9 +559,8 @@ public void paintChildren() {
*/
public void setBorderStyle(byte border) // guich@200final_16
{
- int gap = border == BORDER_NONE || borderStyle == BORDER_TOP ? 0 : borderStyle == BORDER_SIMPLE ? 1 : 2;
- setInsets(gap, gap, gap, gap);
this.borderStyle = border;
+ recomputeInsets();
if (border == BORDER_ROUNDED) {
transparentBackground = true;
}
@@ -611,6 +635,12 @@ protected void fillBackground(Graphics g, int backColor, int foreColor, int x, i
@Override
public void onPaint(Graphics g) {
int b = pressColor != -1 && cpressed ? pressColor : backColor;
+
+ if (controlRenderer != null) {
+ controlRenderer.draw(g, 0, 0, width, height, cpressed);
+ return;
+ }
+
if (drawTranslucentBackground(g, alphaValue)) {
} else if (!transparentBackground
&& (parent != null && (b != parent.backColor || parent.asWindow != null || alwaysEraseBackground))) {
@@ -1108,4 +1138,53 @@ public void setBorderRadius(int borderRadius) {
this.borderRadius = borderRadius;
}
+ @Override
+ protected void onBoundsChanged(boolean screenChanged) {
+ super.onBoundsChanged(screenChanged);
+ updateChildrenClip();
+ }
+
+ @Override
+ protected void onStyleChanged() {
+ recomputeInsets();
+ updateChildrenClip();
+ }
+
+ private void recomputeInsets() {
+ int left = userInsets.left;
+ int right = userInsets.right;
+ int top = userInsets.top;
+ int bottom = userInsets.bottom;
+
+ if (controlRenderer != null) {
+ Insets rendererInsets = controlRenderer.getInsets();
+ left += rendererInsets.left;
+ right += rendererInsets.right;
+ top += rendererInsets.top;
+ bottom += rendererInsets.bottom;
+ } else {
+ int gap = borderStyle == BORDER_NONE || borderStyle == BORDER_TOP ? 0 : borderStyle == BORDER_SIMPLE ? 1 : 2;
+ left += gap;
+ right += gap;
+ top += gap;
+ bottom += gap;
+ }
+
+ insets.left = left;
+ insets.right = right;
+ insets.top = top;
+ insets.bottom = bottom;
+ }
+
+ private void updateChildrenClip() {
+ childrenClip = computeChildrenClip();
+ }
+
+ private RRect computeChildrenClip() {
+ if (controlRenderer != null) {
+ return controlRenderer.getChildrenClip(width, height);
+ }
+ return null;
+ }
+
}
diff --git a/TotalCrossSDK/src/main/java/totalcross/ui/Control.java b/TotalCrossSDK/src/main/java/totalcross/ui/Control.java
index c13a03d18..fd6f7cb1e 100644
--- a/TotalCrossSDK/src/main/java/totalcross/ui/Control.java
+++ b/TotalCrossSDK/src/main/java/totalcross/ui/Control.java
@@ -54,8 +54,11 @@
import totalcross.ui.gfx.Coord;
import totalcross.ui.gfx.GfxSurface;
import totalcross.ui.gfx.Graphics;
+import totalcross.ui.gfx.RRect;
import totalcross.ui.gfx.Rect;
import totalcross.ui.image.Image;
+import totalcross.ui.style.render.ControlRenderer;
+import totalcross.util.UnitsConverter;
import totalcross.util.Vector;
/**
@@ -106,6 +109,7 @@ public static enum TranslucentShape {
public int clearValueInt; // guich@572_19
/** The next control that will receive focus when tab is hit. */
public Control nextTabControl;
+ protected ControlRenderer controlRenderer;
public static final int RANGE = 10000000;
public static final int UICONST = RANGE * 2 + 1000000;
/** Constant used in params width and height in setRect. You can use this constant added to a number to specify a increment/decrement to the calculated size. EG: PREFERRED+2 or PREFERRED-1. */
@@ -469,7 +473,7 @@ void paint2shot(Graphics g, Control top, boolean shift) {
if (child.visible) {
Rect r = child.getAbsoluteRect();
if (rtop.intersects(r)) {
- child.refreshGraphics(g, 0, top, x0, y0);
+ child.refreshGraphics(g, child.getPaintExpand(), top, x0, y0);
child.onPaint(g);
if (child.asContainer != null) {
child.asContainer.paint2shot(g, top, shift);
@@ -1589,7 +1593,7 @@ public void repaintNow() {
} else if (transparentBackground) {
parent.repaintNow(); // guich@tc100: for transparent backgrounds we have to force paint everything
} else {
- Graphics g = refreshGraphics(gfx, 0, null, 0, 0);
+ Graphics g = refreshGraphics(gfx, getPaintExpand(), null, 0, 0);
if (g != null) {
onPaint(g);
if (asContainer != null) {
@@ -1625,13 +1629,22 @@ public void translateFromOrigin(Coord z) // guich@550_31
* It sets a clipping rectangle on the graphics, clipping it against all parent areas.
*/
public Graphics getGraphics() {
- return refreshGraphics(gfx, 0, null, 0, 0);
+ return getGraphics(null);
}
- Graphics refreshGraphics(Graphics g, int expand, Control topParent, int tx0, int ty0) {
+ public Graphics getGraphics(Graphics sourceGraphics) {
+ return refreshGraphics(gfx, getPaintExpand(), null, 0, 0, sourceGraphics);
+ }
+
+ Graphics refreshGraphics(Graphics g, double expand, Control topParent, int tx0, int ty0) {
+ return refreshGraphics(g, expand, topParent, tx0, ty0, null);
+ }
+
+ Graphics refreshGraphics(Graphics g, double expand, Control topParent, int tx0, int ty0, Graphics sourceGraphics) {
if (asWindow == null && parent == null) {
return null;
}
+ int roundedExpand = (int) Math.round(UnitsConverter.toPixels(Control.DP + expand));
int sw = this.width;
int sh = this.height;
int sx = this.x, sy = this.y, cx, cy, delta, tx = sx, ty = sy;
@@ -1667,11 +1680,31 @@ Graphics refreshGraphics(Graphics g, int expand, Control topParent, int tx0, int
}
}
}
- g.refresh(sx + tx0 - expand, sy - expand + ty0, sw + expand + expand, sh + expand + expand, tx + tx0, ty + ty0,
+ g.refresh(sx + tx0 - roundedExpand, sy - roundedExpand + ty0, sw + roundedExpand + roundedExpand, sh + roundedExpand + roundedExpand, tx + tx0, ty + ty0,
font);
+ if (sourceGraphics != null) {
+ RRect inheritedClip = sourceGraphics.getClip();
+ inheritedClip.x -= this.x;
+ inheritedClip.y -= this.y;
+ g.setClip(inheritedClip);
+ }
return g;
}
+ /**
+ * Returns how far this control may paint outside its bounds, in density-independent units.
+ *
+ * The value is used to expand the refreshed graphics area so visual effects such as shadows
+ * are not clipped by the control bounds. The refresh pipeline converts this value to pixels
+ * at the point where the repaint rectangle is computed.
+ *
+ * @return the visual outset required by this control, or {@code 0} when no extra paint area is
+ * needed.
+ */
+ protected double getPaintExpand() {
+ return controlRenderer == null ? 0d : controlRenderer.getOutset();
+ }
+
/**
* Posts an event. The event pass will be posted to this control
* and all the parent controls of this control (all the containers
@@ -1931,6 +1964,39 @@ public void setFocusLess(boolean on) {
public static void resetStyle() {
uiStyleAlreadyChanged = false;
}
+
+ /**
+ * Sets the renderer responsible for drawing this control and providing visual metrics such as
+ * paint outset.
+ *
+ * Passing {@code null} removes the renderer and makes the control fall back to its normal
+ * painting behavior. This method calls {@link #onStyleChanged()} after updating the renderer.
+ *
+ * @param renderer the renderer to associate with this control, or {@code null} to clear it.
+ */
+ public void setControlRenderer(ControlRenderer renderer) {
+ this.controlRenderer = renderer;
+ onStyleChanged();
+ }
+
+ /**
+ * Returns the renderer currently associated with this control.
+ *
+ * @return the current renderer, or {@code null} when this control uses its normal painting
+ * behavior.
+ */
+ public ControlRenderer getControlRenderer() {
+ return controlRenderer;
+ }
+
+ /**
+ * Called after the control renderer or visual style associated with this control changes.
+ *
+ * Subclasses may override this hook to update cached geometry, insets, clips, or any other
+ * derived state that depends on the current style. The default implementation does nothing.
+ */
+ protected void onStyleChanged() {
+ }
/** Internal use only */
public static void uiStyleChanged() {
@@ -2098,7 +2164,7 @@ protected void reposition(boolean recursive) {
font = current;
fm = font.fm;
fmH = font.fm.height;
- refreshGraphics(gfx, 0, null, 0, 0);
+ refreshGraphics(gfx, getPaintExpand(), null, 0, 0);
}
if (recursive) {
repositionChildren();
diff --git a/TotalCrossSDK/src/main/java/totalcross/ui/gfx/Graphics.java b/TotalCrossSDK/src/main/java/totalcross/ui/gfx/Graphics.java
index 36774d1bd..eb3ce9696 100644
--- a/TotalCrossSDK/src/main/java/totalcross/ui/gfx/Graphics.java
+++ b/TotalCrossSDK/src/main/java/totalcross/ui/gfx/Graphics.java
@@ -63,6 +63,7 @@ public final class Graphics {
private int transX, transY;
private int clipX1, clipY1, clipX2, clipY2; // clip1 <= k < clip2
+ private RRect roundClip;
private int minX, minY, maxX, maxY;
@@ -270,6 +271,7 @@ public void refresh(int sx, int sy, int sw, int sh, int tx, int ty, Font f) {
clipY1 = minY = Math.max(0, sy);
clipX2 = maxX = Math.min(sx + sw, scrW);
clipY2 = maxY = Math.min(sy + sh, scrH);
+ roundClip = null;
transX = tx;
transY = ty;
if (f != null) {
@@ -534,7 +536,7 @@ public void fillCircleGradient(int xc, int yc, int r) {
public int getPixel(int x, int y) {
x += transX;
y += transY;
- if (clipX1 <= x && x < clipX2 && clipY1 <= y && y < clipY2) {
+ if (isInsideClip(x, y)) {
return getSurfacePixels(surface)[y * pitch + x] & 0xFFFFFF;
}
return -1;
@@ -573,7 +575,7 @@ private boolean surelyOutsideClip(int x1, int y1, int x2, int y2) {
*/
@ReplacedByNativeOnDeploy
public void drawLine(int Ax, int Ay, int Bx, int By) {
- drawLine(Ax, Ay, Bx, By, foreColor | alpha);
+ drawLine(Ax, Ay, Bx, By, paintOfColor(foreColor | alpha));
}
/**
@@ -639,8 +641,16 @@ public void fillRect(int x, int y, int w, int h) {
return;
}
int[] pix = getSurfacePixels(surface);
- for (x += y * pitch; h-- > 0; x += pitch) {
- Convert.fill(pix, x, x + w, backColor | alpha);
+ int rowX = x;
+ int rowY = y;
+ int color = backColor | alpha;
+ for (int row = 0; row < h; row++, rowY++) {
+ if (!clipHorizontalSpanToCurrentClip(rowY, rowX, rowX + w - 1, translateAndClipResults)) {
+ continue;
+ }
+ int start = translateAndClipResults[0];
+ int width = translateAndClipResults[2];
+ Convert.fill(pix, start + rowY * pitch, start + rowY * pitch + width, color);
}
if (isControlSurface) {
needsUpdate = true;
@@ -695,7 +705,7 @@ public void drawPolygon(int[] xPoints, int[] yPoints, int nPoints) {
throw new ArrayIndexOutOfBoundsException("array index out of range at drawPolygon: " + nPoints);
} else {
drawPolygon(xPoints, yPoints, 0, nPoints, foreColor | alpha);
- drawLine(xPoints[0], yPoints[0], xPoints[nPoints - 1], yPoints[nPoints - 1], foreColor | alpha);
+ drawLine(xPoints[0], yPoints[0], xPoints[nPoints - 1], yPoints[nPoints - 1], paintOfColor(foreColor | alpha));
}
}
@@ -959,13 +969,20 @@ public void drawText(String text, int x, int y, int justifyWidth) {
// draws the char
for (; r < rmax; start += rowWIB, r++, y++) {
+ int rowStart;
+ int rowEnd;
// draw each row
+ if (!clipHorizontalSpanToCurrentClip(y, x0, xMax - 1, translateAndClipResults)) {
+ continue;
+ }
+ rowStart = translateAndClipResults[0];
+ rowEnd = rowStart + translateAndClipResults[2];
int yy = y * pitch;
current = start;
currentBit = startBit;
for (x = x0; x < xMax; x++) {
// draw each column
- if (bitmapTable != null && (bitmapTable[current] & ands8[currentBit]) != 0 && x >= xMin) {
+ if (bitmapTable != null && (bitmapTable[current] & ands8[currentBit]) != 0 && rowStart <= x && x < rowEnd) {
pixels[yy + x] = foreColor | alpha;
}
if (++currentBit == 8) { // finished this byte?
@@ -982,7 +999,14 @@ public void drawText(String text, int x, int y, int justifyWidth) {
int transparency;
// draw the char
for (; r < rmax; start += rowWIB, r++, y++) {
+ int rowStart;
+ int rowEnd;
// draw each row
+ if (!clipHorizontalSpanToCurrentClip(y, x0, xMax - 1, translateAndClipResults)) {
+ continue;
+ }
+ rowStart = translateAndClipResults[0];
+ rowEnd = rowStart + translateAndClipResults[2];
int yy = y * pitch;
current = start;
boolean isLowNibble = isNibbleStartingLow;
@@ -990,7 +1014,7 @@ public void drawText(String text, int x, int y, int justifyWidth) {
// draw each column
transparency = isLowNibble ? (bitmapTable[current++] & 0xF) : ((bitmapTable[current] >> 4) & 0xF);
isLowNibble = !isLowNibble;
- if (transparency == 0 || x < xMin) {
+ if (transparency == 0 || x < rowStart || x >= rowEnd) {
continue;
}
if (transparency == 0xF) {
@@ -1011,13 +1035,20 @@ public void drawText(String text, int x, int y, int justifyWidth) {
int[] imgPixels = font.nativeFonts[bits.index].getPixels();
// draw the char
for (; r < rmax; start += rowWIB, r++, y++) {
+ int rowStart;
+ int rowEnd;
// draw each row
+ if (!clipHorizontalSpanToCurrentClip(y, x0, xMax - 1, translateAndClipResults)) {
+ continue;
+ }
+ rowStart = translateAndClipResults[0];
+ rowEnd = rowStart + translateAndClipResults[2];
int yy = y * pitch;
current = start;
for (x = x0; x < xMax; x++) {
// draw each column
transparency = (imgPixels[current++] >>> 24) & 0xFF;
- if (transparency == 0 || x < xMin) {
+ if (transparency == 0 || x < rowStart || x >= rowEnd) {
continue;
}
if (transparency == 0xFF) {
@@ -1134,8 +1165,8 @@ public void fillRoundRect(int xx, int yy, int width, int height, int r) {
do {
i = 255 - 255 * Math.abs(err - 2 * (x + y) - 2) / r;
- drawLine(px1 + x + 1, py1 - y, px2 - x - 1, py1 - y, c);
- drawLine(px1 + x + 1, py2 + y, px2 - x - 1, py2 + y, c);
+ drawLine(px1 + x + 1, py1 - y, px2 - x - 1, py1 - y, paintOfColor(c));
+ drawLine(px1 + x + 1, py2 + y, px2 - x - 1, py2 + y, paintOfColor(c));
if (i < 256 && i > 0) {
xm = px2;
@@ -1192,6 +1223,74 @@ public void fillRoundRect(int xx, int yy, int width, int height, int r) {
} while (x < 0);
}
+ /**
+ * Draws a rounded rectangle.
+ *
+ * @param rrect the rounded rectangle to draw
+ * @param color the color in {@code 0xRRGGBB} format
+ * @param filled whether the rounded rectangle should be filled
+ * @throws NullPointerException if {@code rrect} is {@code null}
+ * @throws IllegalArgumentException if {@code rrect.width < 0} or {@code rrect.height < 0}
+ */
+ @ReplacedByNativeOnDeploy
+ public void drawRRect(RRect rrect, int color, boolean filled) {
+ if (rrect == null) {
+ throw new NullPointerException("rrect");
+ }
+ if (rrect.width < 0) {
+ throw new IllegalArgumentException("rrect.width must be >= 0");
+ }
+ if (rrect.height < 0) {
+ throw new IllegalArgumentException("rrect.height must be >= 0");
+ }
+ if (rrect.width == 0 || rrect.height == 0) {
+ return;
+ }
+
+ int top = rrect.y;
+ int bottom = rrect.y + rrect.height - 1;
+ int previousStart = Integer.MIN_VALUE;
+ int previousEnd = Integer.MIN_VALUE;
+
+ for (int yy = top; yy <= bottom; yy++) {
+ Span span = rrect.horizontalSpan(yy);
+ if (!span.isValid()) {
+ continue;
+ }
+ int start = span.start;
+ int end = span.end;
+
+ if (filled) {
+ drawLine(start, yy, end, yy, paintOfColor(color));
+ } else {
+ if (previousStart == Integer.MIN_VALUE) {
+ drawLine(start, yy, end, yy, paintOfColor(color));
+ } else {
+ if (start < previousStart) {
+ drawLine(start, yy, previousStart, yy, paintOfColor(color));
+ } else if (start > previousStart) {
+ drawLine(previousStart, yy - 1, start, yy - 1, paintOfColor(color));
+ }
+ if (end > previousEnd) {
+ drawLine(previousEnd, yy, end, yy, paintOfColor(color));
+ } else if (end < previousEnd) {
+ drawLine(end, yy - 1, previousEnd, yy - 1, paintOfColor(color));
+ }
+ }
+
+ setPixel(start, yy, color);
+ setPixel(end, yy, color);
+ }
+
+ previousStart = start;
+ previousEnd = end;
+ }
+
+ if (!filled && previousStart != Integer.MIN_VALUE) {
+ drawLine(previousStart, bottom, previousEnd, bottom, paintOfColor(color));
+ }
+ }
+
/**
* Sets the clipping rectangle, translated to the current translated origin. Anything drawn outside of the
* rectangular area specified will be clipped. Setting a clip overrides any previous clip. This clipping rectangle
@@ -1248,6 +1347,7 @@ public void setClip(int x, int y, int w, int h) {
this.clipY1 = clipY1;
this.clipX2 = clipX2;
this.clipY2 = clipY2;
+ this.roundClip = null;
}
/**
@@ -1261,6 +1361,24 @@ public void setClip(Rect r) {
setClip(r.x, r.y, r.width, r.height);
}
+ /**
+ * Sets the clipping area using a rounded rectangle. Backends without rounded clipping support
+ * currently fall back to the bounding rectangle.
+ *
+ * @param r the rounded rectangle with the clipping coordinates
+ */
+ public void setClip(RRect r) {
+ if (r == null) {
+ clearClip();
+ return;
+ }
+ setClip(r.x, r.y, r.width, r.height);
+ if (r.width <= 0 || r.height <= 0) {
+ return;
+ }
+ this.roundClip = new RRect(r.x + transX, r.y + transY, r.width, r.height, r.radii);
+ }
+
/**
* Gets the current clipping rectangle.
*
@@ -1274,6 +1392,23 @@ public Rect getClip(Rect r) {
return r;
}
+ /**
+ * Gets the current clipping shape. If the current clip is rounded, the radii are preserved;
+ * otherwise this returns the current rectangular clip with zero radii.
+ */
+ public RRect getClip() {
+ if (roundClip != null) {
+ return new RRect(
+ roundClip.x - transX,
+ roundClip.y - transY,
+ roundClip.width,
+ roundClip.height,
+ roundClip.radii);
+ }
+ Rect rect = getClip(new Rect());
+ return new RRect(rect.x, rect.y, rect.width, rect.height, null);
+ }
+
/** Returns the clip's width */
public int getClipWidth() {
return clipX2 - clipX1;
@@ -1310,6 +1445,7 @@ public void clearClip() {
clipY1 = minY;
clipX2 = maxX;
clipY2 = maxY;
+ roundClip = null;
}
/**
@@ -1573,13 +1709,13 @@ public void drawVistaRect(int x, int y, int width, int height, int topColor, int
int x2 = x + width - 1;
int y2 = y + height - 1;
foreColor = topColor | alpha;
- drawLine(x1, y, x2 - 1, y, foreColor);
+ drawLine(x1, y, x2 - 1, y, paintOfColor(foreColor));
foreColor = rightColor | alpha;
- drawLine(x2, y1, x2, y2 - 1, foreColor);
+ drawLine(x2, y1, x2, y2 - 1, paintOfColor(foreColor));
foreColor = bottomColor | alpha;
- drawLine(x1, y2, x2 - 1, y2, foreColor);
+ drawLine(x1, y2, x2 - 1, y2, paintOfColor(foreColor));
foreColor = leftColor | alpha;
- drawLine(x, y1, x, y2 - 1, foreColor);
+ drawLine(x, y1, x, y2 - 1, paintOfColor(foreColor));
}
/**
@@ -1667,12 +1803,12 @@ public void draw3dRect(int x, int y, int width, int height, byte type, boolean y
fillRect(x, y, width, height);
} else if (type == R3D_SHADED) {
boolean menu = simple; // is menu?
- drawLine(menu ? 0 : 1, 0, menu ? width - 1 : width - 3, 0, foreColor | alpha);
- drawLine(0, 1, 0, height - 3, foreColor | alpha);
- drawLine(width - 2, 1, width - 2, height - 3, foreColor | alpha);
- drawLine(width - 1, menu ? 1 : 2, width - 1, height - 3, foreColor | alpha);
- drawLine(1, height - 2, width - 2, height - 2, foreColor | alpha);
- drawLine(2, height - 1, width - 3, height - 1, foreColor | alpha);
+ drawLine(menu ? 0 : 1, 0, menu ? width - 1 : width - 3, 0, paintOfColor(foreColor | alpha));
+ drawLine(0, 1, 0, height - 3, paintOfColor(foreColor | alpha));
+ drawLine(width - 2, 1, width - 2, height - 3, paintOfColor(foreColor | alpha));
+ drawLine(width - 1, menu ? 1 : 2, width - 1, height - 3, paintOfColor(foreColor | alpha));
+ drawLine(1, height - 2, width - 2, height - 2, paintOfColor(foreColor | alpha));
+ drawLine(2, height - 1, width - 3, height - 1, paintOfColor(foreColor | alpha));
} else {
switch (Settings.uiStyle) {
case Settings.Flat:
@@ -1764,7 +1900,7 @@ public void drawArrow(int x, int y, int h, byte type, boolean pressed, int foreC
}
h--;
while (h >= 0) {
- drawLine(x, y, x, y + (h << 1), foreColor);
+ drawLine(x, y, x, y + (h << 1), paintOfColor(foreColor));
x += step;
y++;
h--;
@@ -1776,7 +1912,7 @@ public void drawArrow(int x, int y, int h, byte type, boolean pressed, int foreC
}
h--;
while (h >= 0) {
- drawLine(x, y, x + (h << 1), y, foreColor);
+ drawLine(x, y, x + (h << 1), y, paintOfColor(foreColor));
y += step;
x++;
h--;
@@ -1979,10 +2115,17 @@ public int getRGB(int[] data, int offset, int x, int y, int w, int h) {
h = results[3];
int[] pixels = getSurfacePixels(surface);
- int inc = pitch, pos = y * inc + x, count = w * h;
+ int inc = pitch, count = 0;
- for (; h-- > 0; pos += inc, offset += w) {
- System.arraycopy(pixels, pos, data, offset, w);
+ for (int yy = y; yy < y + h; yy++) {
+ if (!clipHorizontalSpanToCurrentClip(yy, x, x + w - 1, translateAndClipResults)) {
+ continue;
+ }
+ int start = translateAndClipResults[0];
+ int width = translateAndClipResults[2];
+ System.arraycopy(pixels, yy * inc + start, data, offset, width);
+ offset += width;
+ count += width;
}
return count;
@@ -2016,10 +2159,17 @@ public int setRGB(int[] data, int offset, int x, int y, int w, int h) {
h = results[3];
int[] pixels = getSurfacePixels(surface);
- int inc = pitch, pos = y * inc + x, count = w * h;
+ int inc = pitch, count = 0;
- for (; h-- > 0; pos += inc, offset += w) {
- Vm.arrayCopy(data, offset, pixels, pos, w);
+ for (int yy = y; yy < y + h; yy++) {
+ if (!clipHorizontalSpanToCurrentClip(yy, x, x + w - 1, translateAndClipResults)) {
+ continue;
+ }
+ int start = translateAndClipResults[0];
+ int width = translateAndClipResults[2];
+ Vm.arrayCopy(data, offset, pixels, yy * inc + start, width);
+ offset += width;
+ count += width;
}
return count;
@@ -2137,15 +2287,21 @@ private void drawSurface(int[] pixels, Object srcSurface, int x, int y, int widt
int psrc = (bmpY + y) * scrPitch + bmpX + x;
int pdst = dstY * pitch + dstX;
int alphaMask = srcSurface instanceof Image ? ((Image) srcSurface).alphaMask : 255;
- for (j = height; --j >= 0; psrc += scrPitch, pdst += pitch) {
- int srcIdx = psrc; // guich@450_1
- int dstIdx = pdst;
+ for (j = height; --j >= 0; psrc += scrPitch, pdst += pitch, dstY++) {
+ if (!clipHorizontalSpanToCurrentClip(dstY, dstX, dstX + width - 1, translateAndClipResults)) {
+ continue;
+ }
+ int spanStart = translateAndClipResults[0];
+ int spanWidth = translateAndClipResults[2];
+ int skip = spanStart - dstX;
+ int srcIdx = psrc + skip; // guich@450_1
+ int dstIdx = pdst + skip;
if (isSrcScreen) {
- for (i = width; --i >= 0;) {
+ for (i = spanWidth; --i >= 0;) {
dst[dstIdx++] = pixels[srcIdx++] | 0xFF000000;
}
} else {
- for (i = width; --i >= 0; dstIdx++) {
+ for (i = spanWidth; --i >= 0; dstIdx++) {
bmpPt = pixels[srcIdx++];
int a = (bmpPt >>> 24) & 0xFF;
a = alphaMask * a / 255;
@@ -2324,7 +2480,7 @@ private void drawPolygon(int[] xPoints1, int[] yPoints1, int base1, int nPoints1
}
for (i = 1; i < nPoints1; i++) {
- drawLine(xPoints1[base1 + i - 1], yPoints1[base1 + i - 1], xPoints1[base1 + i], yPoints1[base1 + i], c);
+ drawLine(xPoints1[base1 + i - 1], yPoints1[base1 + i - 1], xPoints1[base1 + i], yPoints1[base1 + i], paintOfColor(c));
}
}
@@ -2333,7 +2489,7 @@ private void drawPolygon(int[] xPoints1, int[] yPoints1, int base1, int nPoints1
private void setPixel(int x, int y, int color) {
x += transX;
y += transY;
- if (clipX1 <= x && x < clipX2 && clipY1 <= y && y < clipY2) {
+ if (isInsideClip(x, y)) {
if (x < 0 || y < 0) {
return;
}
@@ -2362,6 +2518,11 @@ private void drawHLine(int x, int y, int w, int color) {
if (x < 0 || y < 0 || w < 0) {
return;
}
+ if (!clipHorizontalSpanToCurrentClip(y, x, x + w - 1, translateAndClipResults)) {
+ return;
+ }
+ x = translateAndClipResults[0];
+ w = translateAndClipResults[2];
int[] pix = getSurfacePixels(surface);
x += y * pitch;
Convert.fill(pix, x, x + w, color);
@@ -2389,8 +2550,11 @@ private void drawVLine(int x, int y, int h, int color) {
return;
}
int[] pixels = getSurfacePixels(surface);
- for (x += y * pitch; h-- > 0; x += pitch) {
- pixels[x] = color;
+ int baseX = x;
+ for (x += y * pitch; h-- > 0; x += pitch, y++) {
+ if (isInsideClip(baseX, y)) {
+ pixels[x] = color;
+ }
}
if (isControlSurface) {
needsUpdate = true;
@@ -2398,9 +2562,22 @@ private void drawVLine(int x, int y, int h, int color) {
}
}
+ /**
+ * Draws a line using the provided paint state.
+ *
+ * The current implementation only reads {@link Paint#getColor()} from the supplied paint.
+ *
+ * @param Ax x coordinate of the start of the line
+ * @param Ay y coordinate of the start of the line
+ * @param Bx x coordinate of the end of the line
+ * @param By y coordinate of the end of the line
+ * @param paint the paint used to draw the line
+ * @throws NullPointerException if {@code paint} is {@code null}
+ */
// Bresenham algorithm to draw lines (modified by guich to improve performance in vertical and horizontal lines)
@ReplacedByNativeOnDeploy
- private void drawLine(int Ax, int Ay, int Bx, int By, int c) {
+ public void drawLine(int Ax, int Ay, int Bx, int By, Paint paint) {
+ int c = paint.getColor();
if (surelyOutsideClip(Ax, Ay, Bx, By)) {
return;
}
@@ -2473,6 +2650,10 @@ private void drawLine(int Ax, int Ay, int Bx, int By, int c) {
}
}
+ private static Paint paintOfColor(int color) {
+ return new Paint().setColor(color);
+ }
+
////////////////////////////////////////////////////////////////////////////
// Bresenham algorithm to draw lines. draws one pixel with color c1 and one pixel with color c2
private void drawDottedLine(int Ax, int Ay, int Bx, int By, int c1, int c2) {
@@ -2576,6 +2757,48 @@ private boolean translateAndClip(int x, int y, int w, int h) {
return true;
}
+ private boolean isInsideClip(int x, int y) {
+ if (!(clipX1 <= x && x < clipX2 && clipY1 <= y && y < clipY2)) {
+ return false;
+ }
+ return roundClip == null || roundClip.contains(x, y);
+ }
+
+ private boolean clipHorizontalSpanToCurrentClip(int y, int start, int end, int[] out) {
+ if (y < clipY1 || y >= clipY2 || end < start) {
+ return false;
+ }
+ if (start < clipX1) {
+ start = clipX1;
+ }
+ if (end >= clipX2) {
+ end = clipX2 - 1;
+ }
+ if (end < start) {
+ return false;
+ }
+ if (roundClip != null) {
+ Span span = roundClip.horizontalSpan(y);
+ if (!span.isValid()) {
+ return false;
+ }
+ if (start < span.start) {
+ start = span.start;
+ }
+ if (end > span.end) {
+ end = span.end;
+ }
+ if (end < start) {
+ return false;
+ }
+ }
+ out[0] = start;
+ out[1] = y;
+ out[2] = end - start + 1;
+ out[3] = 1;
+ return true;
+ }
+
private int doClipX(int x) {
return ((x + transX) < clipX1) ? (clipX1 - transX) : ((x + transX) >= clipX2) ? (clipX2 - transX) : x; // guich@401_2: added TRANSX/Y
}
@@ -2711,9 +2934,6 @@ private void arcPiePointDrawAndFill(int xc, int yc, int rx, int ry, double start
double ppd;
int startIndex, endIndex, index, i, oldX1 = 0, oldY1 = 0, oldX2 = 0, oldY2 = 0;
int nq, size = 0;
- boolean checkClipX = (xc + rx + transX) > clipX2 || (xc - rx + transX) < clipX1; // guich@340_3 - guich@401_2:
- // added TRANSX/Y
- boolean checkClipY = (yc + ry + transY) > clipY2 || (yc - ry + transY) < clipY1;
if (rx < 0 || ry < 0) {
return;
}
@@ -2728,7 +2948,7 @@ private void arcPiePointDrawAndFill(int xc, int yc, int rx, int ry, double start
int[] xPoints = gxPoints;
int[] yPoints = gyPoints;
// step 0: if possible, use cached results
- int clipFactor = clipX1 * 1000000000 + clipX2 * 10000000 + clipY1 * 100000 + clipY2;
+ int clipFactor = 0;
boolean sameClip = clipFactor == lastClipFactor;
boolean sameC = sameClip && xc == lastXC && yc == lastYC;
boolean sameR = sameClip && rx == lastRX && ry == lastRY;
@@ -2799,20 +3019,20 @@ private void arcPiePointDrawAndFill(int xc, int yc, int rx, int ry, double start
// save 4 points using symmetry
// guich@340_3: added clipping
index = nq * 0 + i; // 0/3
- xPoints[index] = checkClipX ? doClipX(xc + x) : (xc + x);
- yPoints[index] = checkClipY ? doClipY(yc - y) : (yc - y);
+ xPoints[index] = xc + x;
+ yPoints[index] = yc - y;
index = nq * 2 - i - 1; // 1/3
- xPoints[index] = checkClipX ? doClipX(xc - x) : (xc - x);
- yPoints[index] = checkClipY ? doClipY(yc - y) : (yc - y);
+ xPoints[index] = xc - x;
+ yPoints[index] = yc - y;
index = nq * 2 + i; // 2/3
- xPoints[index] = checkClipX ? doClipX(xc - x) : (xc - x);
- yPoints[index] = checkClipY ? doClipY(yc + y) : (yc + y);
+ xPoints[index] = xc - x;
+ yPoints[index] = yc + y;
index = nq * 4 - i - 1; // 3/3
- xPoints[index] = checkClipX ? doClipX(xc + x) : (xc + x);
- yPoints[index] = checkClipY ? doClipY(yc + y) : (yc + y);
+ xPoints[index] = xc + x;
+ yPoints[index] = yc + y;
i++;
y++; // always move up here
@@ -2832,20 +3052,20 @@ private void arcPiePointDrawAndFill(int xc, int yc, int rx, int ry, double start
// save 4 points using symmetry
// guich@340_3: added clipping
index = nq * 0 + i; // 0/3
- xPoints[index] = checkClipX ? doClipX(xc + x) : (xc + x);
- yPoints[index] = checkClipY ? doClipY(yc - y) : (yc - y);
+ xPoints[index] = xc + x;
+ yPoints[index] = yc - y;
index = nq * 2 - i - 1; // 1/3
- xPoints[index] = checkClipX ? doClipX(xc - x) : (xc - x);
- yPoints[index] = checkClipY ? doClipY(yc - y) : (yc - y);
+ xPoints[index] = xc - x;
+ yPoints[index] = yc - y;
index = nq * 2 + i; // 2/3
- xPoints[index] = checkClipX ? doClipX(xc - x) : (xc - x);
- yPoints[index] = checkClipY ? doClipY(yc + y) : (yc + y);
+ xPoints[index] = xc - x;
+ yPoints[index] = yc + y;
index = nq * 4 - i - 1; // 3/3
- xPoints[index] = checkClipX ? doClipX(xc + x) : (xc + x);
- yPoints[index] = checkClipY ? doClipY(yc + y) : (yc + y);
+ xPoints[index] = xc + x;
+ yPoints[index] = yc + y;
i++;
x--; // always move left here
@@ -3005,20 +3225,20 @@ public void drawWindowBorder(int xx, int yy, int ww, int hh, int titleH, int foo
kx = x1l;
ky = yy + i;
c = getPixel(kx, ky);
- drawLine(kx, ky, x2r, yy + i, interpolate(borderColorR, borderColorG, borderColorB, c, a)); // top
+ drawLine(kx, ky, x2r, yy + i, paintOfColor(interpolate(borderColorR, borderColorG, borderColorB, c, a))); // top
ky = y2 - i;
c = getPixel(kx, ky);
- drawLine(kx, ky, x2r, y2 - i, interpolate(borderColorR, borderColorG, borderColorB, c, a)); // bottom
+ drawLine(kx, ky, x2r, y2 - i, paintOfColor(interpolate(borderColorR, borderColorG, borderColorB, c, a))); // bottom
kx = xx + i;
ky = y1l;
c = getPixel(kx, ky);
- drawLine(kx, ky, xx + i, y2r, interpolate(borderColorR, borderColorG, borderColorB, c, a)); // left
+ drawLine(kx, ky, xx + i, y2r, paintOfColor(interpolate(borderColorR, borderColorG, borderColorB, c, a))); // left
kx = x2 - i;
c = getPixel(kx, ky);
- drawLine(kx, ky, x2 - i, y2r, interpolate(borderColorR, borderColorG, borderColorB, c, a)); // right
+ drawLine(kx, ky, x2 - i, y2r, paintOfColor(interpolate(borderColorR, borderColorG, borderColorB, c, a))); // right
}
// round corners
for (int j = 0; j < 7; j++) {
@@ -3094,7 +3314,7 @@ public void drawWindowBorder(int xx, int yy, int ww, int hh, int titleH, int foo
// separator
if (drawSeparators && titleH > 0 && titleColor == bodyColor) {
drawLine(rectX1, ty - 1, rectX2, ty - 1,
- interpolate(borderColorR, borderColorG, borderColorB, titleColorR, titleColorG, titleColorB, 64));
+ paintOfColor(interpolate(borderColorR, borderColorG, borderColorB, titleColorR, titleColorG, titleColorB, 64)));
}
// body
backColor = bodyColor | alpha;
@@ -3103,7 +3323,7 @@ public void drawWindowBorder(int xx, int yy, int ww, int hh, int titleH, int foo
// separator
if (drawSeparators && footerH > 0 && bodyColor == footerColor) {
drawLine(rectX1, ty, rectX2, ty,
- interpolate(borderColorR, borderColorG, borderColorB, titleColorR, titleColorG, titleColorB, 64));
+ paintOfColor(interpolate(borderColorR, borderColorG, borderColorB, titleColorR, titleColorG, titleColorB, 64)));
ty++;
footerH--;
}
diff --git a/TotalCrossSDK/src/main/java/totalcross/ui/gfx/Paint.java b/TotalCrossSDK/src/main/java/totalcross/ui/gfx/Paint.java
new file mode 100644
index 000000000..8e44100cb
--- /dev/null
+++ b/TotalCrossSDK/src/main/java/totalcross/ui/gfx/Paint.java
@@ -0,0 +1,377 @@
+// Copyright (C) 2000-2013 SuperWaba Ltda.
+// Copyright (C) 2014-2020 TotalCross Global Mobile Platform Ltda.
+//
+// SPDX-License-Identifier: LGPL-2.1-only
+
+package totalcross.ui.gfx;
+
+/**
+ * Describes low-level drawing parameters in a way similar to Skia's paint state.
+ *
+ * This class is intentionally mutable so a single instance can be configured and
+ * reused across multiple drawing operations.
+ */
+public final class Paint {
+ /**
+ * Stroke and fill styles.
+ */
+ public static final class Style {
+ /** Fills the geometry interior. */
+ public static final int FILL = 0;
+ /** Draws only the geometry outline. */
+ public static final int STROKE = 1;
+ /** Fills and also draws the geometry outline. */
+ public static final int STROKE_AND_FILL = 2;
+
+ private Style() {
+ }
+ }
+
+ /**
+ * Stroke cap styles.
+ */
+ public static final class Cap {
+ /** Flat cap that ends exactly at the segment boundary. */
+ public static final int BUTT = 0;
+ /** Rounded cap centered at the segment boundary. */
+ public static final int ROUND = 1;
+ /** Square cap that extends half the stroke width beyond the segment boundary. */
+ public static final int SQUARE = 2;
+
+ private Cap() {
+ }
+ }
+
+ /**
+ * Stroke join styles.
+ */
+ public static final class Join {
+ /** Sharp corner limited by the stroke miter. */
+ public static final int MITER = 0;
+ /** Rounded corner between segments. */
+ public static final int ROUND = 1;
+ /** Beveled corner between segments. */
+ public static final int BEVEL = 2;
+
+ private Join() {
+ }
+ }
+
+ private int color;
+ private int style;
+ private double strokeWidth;
+ private int strokeCap;
+ private int strokeJoin;
+ private double strokeMiter;
+ private boolean antiAlias;
+ private boolean dither;
+ private PathEffect pathEffect;
+
+ /**
+ * Creates a paint with defaults equivalent to a freshly constructed SkPaint.
+ */
+ public Paint() {
+ reset();
+ }
+
+ /**
+ * Creates a paint copying the state from another instance.
+ *
+ * @param source the paint to copy from
+ * @throws NullPointerException if {@code source} is {@code null}
+ */
+ public Paint(Paint source) {
+ set(source);
+ }
+
+ /**
+ * Restores this paint to its default state.
+ *
+ * @return this paint
+ */
+ public Paint reset() {
+ color = 0xFF000000;
+ style = Style.FILL;
+ strokeWidth = 0;
+ strokeCap = Cap.BUTT;
+ strokeJoin = Join.MITER;
+ strokeMiter = 4;
+ antiAlias = false;
+ dither = false;
+ pathEffect = null;
+ return this;
+ }
+
+ /**
+ * Copies the state from another paint.
+ *
+ * @param source the paint to copy from
+ * @return this paint
+ * @throws NullPointerException if {@code source} is {@code null}
+ */
+ public Paint set(Paint source) {
+ if (source == null) {
+ throw new NullPointerException("source");
+ }
+ color = source.color;
+ style = source.style;
+ strokeWidth = source.strokeWidth;
+ strokeCap = source.strokeCap;
+ strokeJoin = source.strokeJoin;
+ strokeMiter = source.strokeMiter;
+ antiAlias = source.antiAlias;
+ dither = source.dither;
+ pathEffect = source.pathEffect;
+ return this;
+ }
+
+ /**
+ * Returns the ARGB color used by this paint.
+ */
+ public int getColor() {
+ return color;
+ }
+
+ /**
+ * Sets the ARGB color used by this paint.
+ *
+ * @param color the color in {@code 0xAARRGGBB} format
+ * @return this paint
+ */
+ public Paint setColor(int color) {
+ this.color = color;
+ return this;
+ }
+
+ /**
+ * Sets the color using separate alpha, red, green and blue channels.
+ *
+ * @param alpha the alpha channel in the range 0-255
+ * @param red the red channel in the range 0-255
+ * @param green the green channel in the range 0-255
+ * @param blue the blue channel in the range 0-255
+ * @return this paint
+ * @throws IllegalArgumentException if any component is outside the range 0-255
+ */
+ public Paint setARGB(int alpha, int red, int green, int blue) {
+ validateChannel("alpha", alpha);
+ validateChannel("red", red);
+ validateChannel("green", green);
+ validateChannel("blue", blue);
+ this.color = (alpha << 24) | Color.getRGB(red, green, blue);
+ return this;
+ }
+
+ /**
+ * Returns the alpha channel in the range 0-255.
+ */
+ public int getAlpha() {
+ return (color >>> 24) & 0xFF;
+ }
+
+ /**
+ * Sets the alpha channel in the range 0-255.
+ *
+ * @param alpha the alpha channel value
+ * @return this paint
+ * @throws IllegalArgumentException if {@code alpha} is outside the range 0-255
+ */
+ public Paint setAlpha(int alpha) {
+ validateChannel("alpha", alpha);
+ this.color = (color & 0x00FFFFFF) | (alpha << 24);
+ return this;
+ }
+
+ /**
+ * Returns the drawing style.
+ */
+ public int getStyle() {
+ return style;
+ }
+
+ /**
+ * Sets the drawing style.
+ *
+ * @param style one of {@link Style#FILL}, {@link Style#STROKE}, or {@link Style#STROKE_AND_FILL}
+ * @return this paint
+ * @throws IllegalArgumentException if {@code style} is invalid
+ */
+ public Paint setStyle(int style) {
+ if (!isValidStyle(style)) {
+ throw new IllegalArgumentException("Invalid paint style: " + style);
+ }
+ this.style = style;
+ return this;
+ }
+
+ /**
+ * Returns the stroke width.
+ */
+ public double getStrokeWidth() {
+ return strokeWidth;
+ }
+
+ /**
+ * Sets the stroke width.
+ *
+ * @param strokeWidth the stroke width, which must be non-negative
+ * @return this paint
+ * @throws IllegalArgumentException if {@code strokeWidth} is negative
+ */
+ public Paint setStrokeWidth(double strokeWidth) {
+ validateNonNegative("strokeWidth", strokeWidth);
+ this.strokeWidth = strokeWidth;
+ return this;
+ }
+
+ /**
+ * Returns the stroke cap.
+ */
+ public int getStrokeCap() {
+ return strokeCap;
+ }
+
+ /**
+ * Sets the stroke cap.
+ *
+ * @param strokeCap one of {@link Cap#BUTT}, {@link Cap#ROUND}, or {@link Cap#SQUARE}
+ * @return this paint
+ * @throws IllegalArgumentException if {@code strokeCap} is invalid
+ */
+ public Paint setStrokeCap(int strokeCap) {
+ if (!isValidCap(strokeCap)) {
+ throw new IllegalArgumentException("Invalid stroke cap: " + strokeCap);
+ }
+ this.strokeCap = strokeCap;
+ return this;
+ }
+
+ /**
+ * Returns the stroke join.
+ */
+ public int getStrokeJoin() {
+ return strokeJoin;
+ }
+
+ /**
+ * Sets the stroke join.
+ *
+ * @param strokeJoin one of {@link Join#MITER}, {@link Join#ROUND}, or {@link Join#BEVEL}
+ * @return this paint
+ * @throws IllegalArgumentException if {@code strokeJoin} is invalid
+ */
+ public Paint setStrokeJoin(int strokeJoin) {
+ if (!isValidJoin(strokeJoin)) {
+ throw new IllegalArgumentException("Invalid stroke join: " + strokeJoin);
+ }
+ this.strokeJoin = strokeJoin;
+ return this;
+ }
+
+ /**
+ * Returns the stroke miter limit.
+ */
+ public double getStrokeMiter() {
+ return strokeMiter;
+ }
+
+ /**
+ * Sets the stroke miter limit.
+ *
+ * @param strokeMiter the miter limit, which must be non-negative
+ * @return this paint
+ * @throws IllegalArgumentException if {@code strokeMiter} is negative
+ */
+ public Paint setStrokeMiter(double strokeMiter) {
+ validateNonNegative("strokeMiter", strokeMiter);
+ this.strokeMiter = strokeMiter;
+ return this;
+ }
+
+ /**
+ * Returns whether anti-aliasing is enabled.
+ */
+ public boolean isAntiAlias() {
+ return antiAlias;
+ }
+
+ /**
+ * Enables or disables anti-aliasing.
+ *
+ * @param antiAlias whether anti-aliasing should be enabled
+ * @return this paint
+ */
+ public Paint setAntiAlias(boolean antiAlias) {
+ this.antiAlias = antiAlias;
+ return this;
+ }
+
+ /**
+ * Returns whether dithering is enabled.
+ */
+ public boolean isDither() {
+ return dither;
+ }
+
+ /**
+ * Enables or disables dithering.
+ *
+ * @param dither whether dithering should be enabled
+ * @return this paint
+ */
+ public Paint setDither(boolean dither) {
+ this.dither = dither;
+ return this;
+ }
+
+ /**
+ * Returns the path effect applied to geometry drawn with this paint, or {@code null} if none is configured.
+ */
+ public PathEffect getPathEffect() {
+ return pathEffect;
+ }
+
+ /**
+ * Sets the path effect applied to geometry drawn with this paint.
+ *
+ * @param pathEffect the path effect to use, or {@code null} to clear it
+ * @return this paint
+ */
+ public Paint setPathEffect(PathEffect pathEffect) {
+ this.pathEffect = pathEffect;
+ return this;
+ }
+
+ /**
+ * Returns whether the given value is a valid paint style.
+ */
+ public static boolean isValidStyle(int style) {
+ return style == Style.FILL || style == Style.STROKE || style == Style.STROKE_AND_FILL;
+ }
+
+ /**
+ * Returns whether the given value is a valid stroke cap.
+ */
+ public static boolean isValidCap(int cap) {
+ return cap == Cap.BUTT || cap == Cap.ROUND || cap == Cap.SQUARE;
+ }
+
+ /**
+ * Returns whether the given value is a valid stroke join.
+ */
+ public static boolean isValidJoin(int join) {
+ return join == Join.MITER || join == Join.ROUND || join == Join.BEVEL;
+ }
+
+ private static void validateChannel(String name, int value) {
+ if (value < 0 || value > 0xFF) {
+ throw new IllegalArgumentException(name + " must be in the range 0-255: " + value);
+ }
+ }
+
+ private static void validateNonNegative(String name, double value) {
+ if (value < 0) {
+ throw new IllegalArgumentException(name + " must be >= 0: " + value);
+ }
+ }
+}
diff --git a/TotalCrossSDK/src/main/java/totalcross/ui/gfx/PathEffect.java b/TotalCrossSDK/src/main/java/totalcross/ui/gfx/PathEffect.java
new file mode 100644
index 000000000..b53a27344
--- /dev/null
+++ b/TotalCrossSDK/src/main/java/totalcross/ui/gfx/PathEffect.java
@@ -0,0 +1,67 @@
+// Copyright (C) 2000-2013 SuperWaba Ltda.
+// Copyright (C) 2014-2020 TotalCross Global Mobile Platform Ltda.
+//
+// SPDX-License-Identifier: LGPL-2.1-only
+
+package totalcross.ui.gfx;
+
+import java.util.Arrays;
+
+/**
+ * Describes geometric effects applied to stroked paths, in a role similar to Skia's path effects.
+ *
+ * This class is immutable. Use one of the factory methods to create an instance.
+ */
+public final class PathEffect {
+ /**
+ * Supported path effect types.
+ */
+ public static final class Type {
+ /** Dash path effect. */
+ public static final int DASH = 0;
+
+ private Type() {
+ }
+ }
+
+ public final int type;
+ public final double[] intervals;
+ public final double phase;
+
+ private PathEffect(int type, double[] intervals, double phase) {
+ this.type = type;
+ this.intervals = intervals;
+ this.phase = phase;
+ }
+
+ /**
+ * Creates a dash path effect.
+ *
+ * @param intervals alternating on/off lengths; each value must be positive and the array length must be even
+ * @param phase the dash phase
+ * @return a new dash path effect
+ * @throws NullPointerException if {@code intervals} is {@code null}
+ * @throws IllegalArgumentException if the intervals array is empty, has odd length, or contains non-positive values
+ */
+ public static PathEffect dash(double[] intervals, double phase) {
+ if (intervals == null) {
+ throw new NullPointerException("intervals");
+ }
+ if (intervals.length == 0 || (intervals.length & 1) != 0) {
+ throw new IllegalArgumentException("Dash intervals must have a positive even length");
+ }
+ for (int i = 0; i < intervals.length; i++) {
+ if (!(intervals[i] > 0)) {
+ throw new IllegalArgumentException("Dash intervals must be > 0: intervals[" + i + "]=" + intervals[i]);
+ }
+ }
+ return new PathEffect(Type.DASH, Arrays.copyOf(intervals, intervals.length), phase);
+ }
+
+ /**
+ * Returns whether the given value is a valid path effect type.
+ */
+ public static boolean isValidType(int type) {
+ return type == Type.DASH;
+ }
+}
diff --git a/TotalCrossSDK/src/main/java/totalcross/ui/gfx/RRect.java b/TotalCrossSDK/src/main/java/totalcross/ui/gfx/RRect.java
new file mode 100644
index 000000000..03a656c02
--- /dev/null
+++ b/TotalCrossSDK/src/main/java/totalcross/ui/gfx/RRect.java
@@ -0,0 +1,269 @@
+// Copyright (C) 2000-2013 SuperWaba Ltda.
+// Copyright (C) 2014-2020 TotalCross Global Mobile Platform Ltda.
+//
+// SPDX-License-Identifier: LGPL-2.1-only
+
+package totalcross.ui.gfx;
+
+import com.totalcross.annotations.ReplacedByNativeOnDeploy;
+import java.util.Arrays;
+
+/**
+ * Describes a rounded rectangle.
+ *
+ * The {@link #radii} array is always normalized to 8 components, storing
+ * {@code rx, ry} for each corner in clockwise order:
+ * top-left, top-right, bottom-right, bottom-left.
+ *
+ * The constructor accepts radii arrays with 1, 2, 4, or 8 values:
+ *
+ * - 1 value: same {@code rx} and {@code ry} for all corners
+ * - 2 values: first is {@code rx}, second is {@code ry}, repeated for all corners
+ * - 4 values: one radius per corner, with {@code rx == ry}
+ * - 8 values: explicit {@code rx, ry} for each corner
+ *
+ */
+public class RRect extends Rect {
+ private static final double[] ZERO_RADII = new double[] {0, 0, 0, 0, 0, 0, 0, 0};
+
+ /**
+ * Corner radii normalized to 8 components in the order
+ * top-left, top-right, bottom-right, bottom-left, each one as {@code rx, ry}.
+ */
+ public final double[] radii;
+
+ /**
+ * Creates a rounded rectangle.
+ *
+ * @param x the x coordinate
+ * @param y the y coordinate
+ * @param width the width
+ * @param height the height
+ * @param radii the corner radii in 1, 2, 4, or 8-component form
+ */
+ public RRect(int x, int y, int width, int height, double[] radii) {
+ super(x, y, width, height);
+ this.radii = normalizeRadii(radii);
+ }
+
+ /**
+ * Creates a rounded rectangle with the same radius on every corner axis.
+ *
+ * @param x the x coordinate
+ * @param y the y coordinate
+ * @param width the width
+ * @param height the height
+ * @param radius the radius repeated across all corner components
+ */
+ public RRect(int x, int y, int width, int height, double radius) {
+ this(x, y, width, height, new double[] {radius});
+ }
+
+ /**
+ * Creates a rounded rectangle with zero radii on every corner.
+ *
+ * @param x the x coordinate
+ * @param y the y coordinate
+ * @param width the width
+ * @param height the height
+ */
+ public RRect(int x, int y, int width, int height) {
+ this(x, y, width, height, 0d);
+ }
+
+ private static double[] normalizeRadii(double[] radii) {
+ if (radii == null) {
+ double[] copy = new double[8];
+ System.arraycopy(ZERO_RADII, 0, copy, 0, 8);
+ return copy;
+ }
+
+ switch (radii.length) {
+ case 1:
+ return new double[] {
+ radii[0], radii[0],
+ radii[0], radii[0],
+ radii[0], radii[0],
+ radii[0], radii[0]
+ };
+ case 2:
+ return new double[] {
+ radii[0], radii[1],
+ radii[0], radii[1],
+ radii[0], radii[1],
+ radii[0], radii[1]
+ };
+ case 4:
+ return new double[] {
+ radii[0], radii[0],
+ radii[1], radii[1],
+ radii[2], radii[2],
+ radii[3], radii[3]
+ };
+ case 8:
+ double[] copy = new double[8];
+ System.arraycopy(radii, 0, copy, 0, 8);
+ return copy;
+ default:
+ throw new IllegalArgumentException(
+ "RRect.radii must have length 1, 2, 4, or 8"
+ );
+ }
+ }
+
+ /**
+ * Returns whether the given point is inside this rounded rectangle.
+ */
+ @Override
+ public boolean contains(int xx, int yy) {
+ double px = xx + 0.5d;
+ double py = yy + 0.5d;
+ double left = x;
+ double top = y;
+ double right = x + width;
+ double bottom = y + height;
+
+ if (px < left || px >= right || py < top || py >= bottom) {
+ return false;
+ }
+
+ return containsCorner(px, py, left, top, radii[0], radii[1], 0)
+ && containsCorner(px, py, right, top, radii[2], radii[3], 1)
+ && containsCorner(px, py, right, bottom, radii[4], radii[5], 2)
+ && containsCorner(px, py, left, bottom, radii[6], radii[7], 3);
+ }
+
+ @ReplacedByNativeOnDeploy
+ private static boolean containsCorner(double px, double py, double x, double y, double rx, double ry, int corner) {
+ if (rx <= 0 || ry <= 0) {
+ return true;
+ }
+
+ double cx = (corner == 0 || corner == 3) ? x + rx : x - rx;
+ double cy = (corner == 0 || corner == 1) ? y + ry : y - ry;
+ boolean inCornerBox =
+ (corner == 0 && px < x + rx && py < y + ry) ||
+ (corner == 1 && px >= x - rx && py < y + ry) ||
+ (corner == 2 && px >= x - rx && py >= y - ry) ||
+ (corner == 3 && px < x + rx && py >= y - ry);
+
+ if (!inCornerBox) {
+ return true;
+ }
+
+ double dx = (px - cx) / rx;
+ double dy = (py - cy) / ry;
+ return dx * dx + dy * dy <= 1d;
+ }
+
+ /**
+ * Computes the horizontal span occupied by this rounded rectangle for a given scanline.
+ *
+ * @param yy the scanline y coordinate
+ * @return the horizontal span for the scanline, or an invalid span if the scanline does not intersect this shape
+ */
+ public Span horizontalSpan(int yy) {
+ double py = yy + 0.5d;
+ double left = x;
+ double top = y;
+ double right = x + width;
+ double bottom = y + height;
+
+ if (py < top || py >= bottom) {
+ return new Span(1, 0);
+ }
+
+ double start = Math.max(left, leftBoundForY(py, left, top, bottom, radii[0], radii[1], radii[6], radii[7]));
+ double end = Math.min(right, rightBoundForY(py, right, top, bottom, radii[2], radii[3], radii[4], radii[5]));
+
+ return new Span((int) Math.ceil(start - 0.5d), (int) Math.floor(end - 0.5d));
+ }
+
+ @ReplacedByNativeOnDeploy
+ private static double leftBoundForY(double py, double left, double top, double bottom,
+ double topRx, double topRy, double bottomRx, double bottomRy) {
+ if (topRx > 0 && topRy > 0 && py < top + topRy) {
+ return ellipseLeftBound(left + topRx, top + topRy, topRx, topRy, py);
+ }
+ if (bottomRx > 0 && bottomRy > 0 && py >= bottom - bottomRy) {
+ return ellipseLeftBound(left + bottomRx, bottom - bottomRy, bottomRx, bottomRy, py);
+ }
+ return left;
+ }
+
+ @ReplacedByNativeOnDeploy
+ private static double rightBoundForY(double py, double right, double top, double bottom,
+ double topRx, double topRy, double bottomRx, double bottomRy) {
+ if (topRx > 0 && topRy > 0 && py < top + topRy) {
+ return ellipseRightBound(right - topRx, top + topRy, topRx, topRy, py);
+ }
+ if (bottomRx > 0 && bottomRy > 0 && py >= bottom - bottomRy) {
+ return ellipseRightBound(right - bottomRx, bottom - bottomRy, bottomRx, bottomRy, py);
+ }
+ return right;
+ }
+
+ @ReplacedByNativeOnDeploy
+ private static double ellipseLeftBound(double cx, double cy, double rx, double ry, double py) {
+ return cx - ellipseDx(rx, ry, py - cy);
+ }
+
+ @ReplacedByNativeOnDeploy
+ private static double ellipseRightBound(double cx, double cy, double rx, double ry, double py) {
+ return cx + ellipseDx(rx, ry, py - cy);
+ }
+
+ @ReplacedByNativeOnDeploy
+ private static double ellipseDx(double rx, double ry, double dy) {
+ if (rx <= 0 || ry <= 0) {
+ return 0d;
+ }
+ double t = 1d - (dy * dy) / (ry * ry);
+ if (t <= 0d) {
+ return 0d;
+ }
+ return rx * Math.sqrt(t);
+ }
+
+ /**
+ * Returns a modified copy of this rounded rectangle.
+ */
+ @Override
+ public RRect modifiedBy(int deltaX, int deltaY, int deltaW, int deltaH) {
+ return new RRect(x + deltaX, y + deltaY, width + deltaW, height + deltaH, radii);
+ }
+
+ /**
+ * Compares this rounded rectangle with another object.
+ */
+ @Override
+ public boolean equals(Object other) {
+ if (!(other instanceof RRect)) {
+ return false;
+ }
+ RRect r = (RRect) other;
+ return r.x == this.x
+ && r.y == this.y
+ && r.width == this.width
+ && r.height == this.height
+ && Arrays.equals(r.radii, this.radii);
+ }
+
+ /**
+ * Returns the hash code for this rounded rectangle.
+ */
+ @Override
+ public int hashCode() {
+ int result = super.hashCode();
+ result = 31 * result + Arrays.hashCode(radii);
+ return result;
+ }
+
+ /**
+ * Returns a string representation of this rounded rectangle.
+ */
+ @Override
+ public String toString() {
+ return super.toString() + ",radii=" + Arrays.toString(radii);
+ }
+}
diff --git a/TotalCrossSDK/src/main/java/totalcross/ui/gfx/Span.java b/TotalCrossSDK/src/main/java/totalcross/ui/gfx/Span.java
new file mode 100644
index 000000000..5801b40c3
--- /dev/null
+++ b/TotalCrossSDK/src/main/java/totalcross/ui/gfx/Span.java
@@ -0,0 +1,36 @@
+// Copyright (C) 2000-2013 SuperWaba Ltda.
+// Copyright (C) 2014-2020 TotalCross Global Mobile Platform Ltda.
+//
+// SPDX-License-Identifier: LGPL-2.1-only
+
+package totalcross.ui.gfx;
+
+/**
+ * Represents a horizontal span on a scanline.
+ */
+public final class Span {
+ /** Inclusive start coordinate of the span. */
+ public final int start;
+ /** Inclusive end coordinate of the span. */
+ public final int end;
+
+ /**
+ * Creates a span.
+ *
+ * @param start inclusive start coordinate
+ * @param end inclusive end coordinate
+ */
+ public Span(int start, int end) {
+ this.start = start;
+ this.end = end;
+ }
+
+ /**
+ * Returns whether this span contains at least one valid pixel.
+ *
+ * @return {@code true} when {@code end >= start}
+ */
+ public boolean isValid() {
+ return end >= start;
+ }
+}
diff --git a/TotalCrossSDK/src/main/java/totalcross/ui/style/css/BoxStyleCssParser.java b/TotalCrossSDK/src/main/java/totalcross/ui/style/css/BoxStyleCssParser.java
new file mode 100644
index 000000000..9101fec49
--- /dev/null
+++ b/TotalCrossSDK/src/main/java/totalcross/ui/style/css/BoxStyleCssParser.java
@@ -0,0 +1,908 @@
+// Copyright (C) 2026 Amalgam Solucoes em TI Ltda
+//
+// SPDX-License-Identifier: LGPL-2.1-only
+
+package totalcross.ui.style.css;
+
+import java.util.ArrayList;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Locale;
+import java.util.Map;
+
+import totalcross.sys.Settings;
+import totalcross.ui.Control;
+import totalcross.ui.Insets;
+import totalcross.ui.gfx.Color;
+import totalcross.ui.style.render.BoxRenderer;
+import totalcross.ui.style.model.BorderSide;
+import totalcross.ui.style.model.BoxBorder;
+import totalcross.ui.style.model.BoxClip;
+import totalcross.ui.style.model.BoxLayout;
+import totalcross.ui.style.model.BoxPaint;
+import totalcross.ui.style.model.BoxShape;
+import totalcross.ui.style.model.BoxStyle;
+import totalcross.ui.style.model.CornerRadii;
+import totalcross.ui.style.model.Elevation;
+import totalcross.ui.style.model.Shadow;
+import totalcross.util.UnitsConverter;
+
+/**
+ * Parses a CSS-like subset into a {@link BoxStyle}.
+ */
+public final class BoxStyleCssParser {
+ private BoxStyleCssParser() {
+ }
+
+ /**
+ * Parses the given CSS-like declaration block into a box style.
+ */
+ public static BoxStyle parse(String css) {
+ String body = extractRuleBody(css);
+ return parseBody(body, new BoxStyle());
+ }
+
+ /**
+ * Parses normal and {@code :active} rules for the given selector into a box renderer.
+ */
+ public static BoxRenderer parseRenderer(String css, String selector) {
+ String baseBody = findRuleBody(css, selector);
+ if (baseBody == null) {
+ baseBody = extractRuleBody(css);
+ }
+
+ BoxStyle baseStyle = parseBody(baseBody, new BoxStyle());
+ String activeBody = findRuleBody(css, selector + ":active");
+ if (activeBody == null) {
+ return new BoxRenderer(baseStyle);
+ }
+
+ return new BoxRenderer(baseStyle, parseBody(activeBody, baseStyle));
+ }
+
+ private static BoxStyle parseBody(String body, BoxStyle base) {
+ BorderDecl top = BorderDecl.from(base.paint.border.top);
+ BorderDecl right = BorderDecl.from(base.paint.border.right);
+ BorderDecl bottom = BorderDecl.from(base.paint.border.bottom);
+ BorderDecl left = BorderDecl.from(base.paint.border.left);
+
+ CornerRadii radii = base.shape.radii;
+ Insets padding = new Insets(
+ base.layout.padding.top,
+ base.layout.padding.left,
+ base.layout.padding.bottom,
+ base.layout.padding.right
+ );
+ int backgroundColor = base.paint.backgroundColor;
+ int pressedColor = base.paint.pressedColor;
+ int overflow = base.clip.overflow;
+ Elevation elevation = base.elevation;
+
+ for (String decl : splitDeclarations(body)) {
+ int colon = decl.indexOf(':');
+ if (colon <= 0) {
+ continue;
+ }
+
+ String property = normalize(decl.substring(0, colon));
+ String value = decl.substring(colon + 1).trim();
+ if (value.isEmpty()) {
+ continue;
+ }
+
+ switch (property) {
+ case "border":
+ applyBorderShorthand(value, top, right, bottom, left);
+ break;
+ case "border-top":
+ applyBorderSideShorthand(value, top);
+ break;
+ case "border-right":
+ applyBorderSideShorthand(value, right);
+ break;
+ case "border-bottom":
+ applyBorderSideShorthand(value, bottom);
+ break;
+ case "border-left":
+ applyBorderSideShorthand(value, left);
+ break;
+ case "border-width":
+ applyWidths(parse1To4BorderWidths(value), top, right, bottom, left);
+ break;
+ case "border-style":
+ applyStyles(parse1To4Keywords(value), top, right, bottom, left);
+ break;
+ case "border-color":
+ applyColors(parse1To4Colors(value), top, right, bottom, left);
+ break;
+ case "border-top-width":
+ top.width = parseLengthDouble(value);
+ break;
+ case "border-right-width":
+ right.width = parseLengthDouble(value);
+ break;
+ case "border-bottom-width":
+ bottom.width = parseLengthDouble(value);
+ break;
+ case "border-left-width":
+ left.width = parseLengthDouble(value);
+ break;
+ case "border-top-style":
+ top.style = parseBorderStyle(value);
+ break;
+ case "border-right-style":
+ right.style = parseBorderStyle(value);
+ break;
+ case "border-bottom-style":
+ bottom.style = parseBorderStyle(value);
+ break;
+ case "border-left-style":
+ left.style = parseBorderStyle(value);
+ break;
+ case "border-top-color":
+ top.color = parseColor(value);
+ break;
+ case "border-right-color":
+ right.color = parseColor(value);
+ break;
+ case "border-bottom-color":
+ bottom.color = parseColor(value);
+ break;
+ case "border-left-color":
+ left.color = parseColor(value);
+ break;
+ case "border-radius":
+ radii = applyBorderRadius(value);
+ break;
+ case "border-top-left-radius":
+ radii = CornerRadii.of(
+ parseLengthPixelsDouble(firstToken(value)),
+ parseLengthPixelsDouble(firstToken(value)),
+ radii.topRightX,
+ radii.topRightY,
+ radii.bottomRightX,
+ radii.bottomRightY,
+ radii.bottomLeftX,
+ radii.bottomLeftY
+ );
+ break;
+ case "border-top-right-radius":
+ radii = CornerRadii.of(
+ radii.topLeftX,
+ radii.topLeftY,
+ parseLengthPixelsDouble(firstToken(value)),
+ parseLengthPixelsDouble(firstToken(value)),
+ radii.bottomRightX,
+ radii.bottomRightY,
+ radii.bottomLeftX,
+ radii.bottomLeftY
+ );
+ break;
+ case "border-bottom-right-radius":
+ radii = CornerRadii.of(
+ radii.topLeftX,
+ radii.topLeftY,
+ radii.topRightX,
+ radii.topRightY,
+ parseLengthPixelsDouble(firstToken(value)),
+ parseLengthPixelsDouble(firstToken(value)),
+ radii.bottomLeftX,
+ radii.bottomLeftY
+ );
+ break;
+ case "border-bottom-left-radius":
+ radii = CornerRadii.of(
+ radii.topLeftX,
+ radii.topLeftY,
+ radii.topRightX,
+ radii.topRightY,
+ radii.bottomRightX,
+ radii.bottomRightY,
+ parseLengthPixelsDouble(firstToken(value)),
+ parseLengthPixelsDouble(firstToken(value))
+ );
+ break;
+ case "background":
+ case "background-color":
+ Integer bg = tryParseBackgroundColor(value);
+ if (bg != null) {
+ backgroundColor = bg.intValue();
+ }
+ break;
+ case "padding":
+ applyPadding(padding, normalize1To4Ints(parseLengths(value)));
+ break;
+ case "padding-top":
+ padding.top = parseLength(value);
+ break;
+ case "padding-right":
+ padding.right = parseLength(value);
+ break;
+ case "padding-bottom":
+ padding.bottom = parseLength(value);
+ break;
+ case "padding-left":
+ padding.left = parseLength(value);
+ break;
+ case "overflow":
+ overflow = parseOverflow(value);
+ break;
+ case "box-shadow":
+ elevation = parseBoxShadow(value);
+ break;
+ default:
+ break;
+ }
+ }
+
+ BoxShape shape = BoxShape.builder()
+ .radii(radii)
+ .build();
+ BoxLayout layout = BoxLayout.builder()
+ .padding(padding)
+ .build();
+ BoxClip clip = BoxClip.builder()
+ .overflow(overflow)
+ .build();
+ BoxBorder border = BoxBorder.builder()
+ .top(top.toBorderSide())
+ .right(right.toBorderSide())
+ .bottom(bottom.toBorderSide())
+ .left(left.toBorderSide())
+ .build();
+ BoxPaint paint = BoxPaint.builder()
+ .backgroundColor(backgroundColor)
+ .pressedColor(pressedColor)
+ .border(border)
+ .build();
+
+ return BoxStyle.builder()
+ .layout(layout)
+ .shape(shape)
+ .paint(paint)
+ .clip(clip)
+ .elevation(elevation)
+ .build();
+ }
+
+ private static void applyPadding(Insets padding, int[] values) {
+ padding.top = values[0];
+ padding.right = values[1];
+ padding.bottom = values[2];
+ padding.left = values[3];
+ }
+
+ private static String extractRuleBody(String css) {
+ if (css == null) {
+ return "";
+ }
+ String s = removeComments(css).trim();
+ int open = s.indexOf('{');
+ int close = findMatchingBrace(s, open);
+ if (open >= 0 && close > open) {
+ return s.substring(open + 1, close).trim();
+ }
+ return s;
+ }
+
+ private static String findRuleBody(String css, String selector) {
+ if (css == null || selector == null) {
+ return null;
+ }
+
+ String s = removeComments(css).trim();
+ int searchFrom = 0;
+ while (searchFrom < s.length()) {
+ int open = s.indexOf('{', searchFrom);
+ if (open < 0) {
+ return null;
+ }
+
+ int close = findMatchingBrace(s, open);
+ if (close < 0) {
+ return null;
+ }
+
+ String selectorList = s.substring(searchFrom, open).trim();
+ if (selectorListContains(selectorList, selector)) {
+ return s.substring(open + 1, close).trim();
+ }
+ searchFrom = close + 1;
+ }
+ return null;
+ }
+
+ private static int findMatchingBrace(String s, int open) {
+ if (open < 0) {
+ return -1;
+ }
+
+ int depth = 0;
+ for (int i = open; i < s.length(); i++) {
+ char ch = s.charAt(i);
+ if (ch == '{') {
+ depth++;
+ } else if (ch == '}') {
+ depth--;
+ if (depth == 0) {
+ return i;
+ }
+ }
+ }
+ return -1;
+ }
+
+ private static boolean selectorListContains(String selectorList, String selector) {
+ List selectors = splitTopLevelCommaSeparated(selectorList);
+ for (int i = 0; i < selectors.size(); i++) {
+ if (normalize(selectors.get(i)).equals(normalize(selector))) {
+ return true;
+ }
+ }
+ return false;
+ }
+
+ private static String removeComments(String s) {
+ StringBuilder out = new StringBuilder(s.length());
+ int i = 0;
+ while (i < s.length()) {
+ if (i + 1 < s.length() && s.charAt(i) == '/' && s.charAt(i + 1) == '*') {
+ i += 2;
+ while (i + 1 < s.length() && !(s.charAt(i) == '*' && s.charAt(i + 1) == '/')) {
+ i++;
+ }
+ i = Math.min(i + 2, s.length());
+ } else {
+ out.append(s.charAt(i++));
+ }
+ }
+ return out.toString();
+ }
+
+ private static List splitDeclarations(String body) {
+ List list = new ArrayList();
+ StringBuilder cur = new StringBuilder();
+ int parenDepth = 0;
+
+ for (int i = 0; i < body.length(); i++) {
+ char ch = body.charAt(i);
+ if (ch == '(') {
+ parenDepth++;
+ } else if (ch == ')') {
+ parenDepth = Math.max(0, parenDepth - 1);
+ }
+
+ if (ch == ';' && parenDepth == 0) {
+ String item = cur.toString().trim();
+ if (!item.isEmpty()) {
+ list.add(item);
+ }
+ cur.setLength(0);
+ } else {
+ cur.append(ch);
+ }
+ }
+
+ String item = cur.toString().trim();
+ if (!item.isEmpty()) {
+ list.add(item);
+ }
+ return list;
+ }
+
+ private static String normalize(String s) {
+ return s.trim().toLowerCase(/* Locale.ROOT */);
+ }
+
+ private static void applyBorderShorthand(String value, BorderDecl top, BorderDecl right, BorderDecl bottom, BorderDecl left) {
+ BorderDecl d = new BorderDecl();
+ applyBorderSideShorthand(value, d);
+ copyDecl(d, top);
+ copyDecl(d, right);
+ copyDecl(d, bottom);
+ copyDecl(d, left);
+ }
+
+ private static void applyBorderSideShorthand(String value, BorderDecl decl) {
+ String[] tokens = tokenize(value);
+ for (int i = 0; i < tokens.length; i++) {
+ String t = normalize(tokens[i]);
+ if (isBorderStyleToken(t)) {
+ decl.style = parseBorderStyle(t);
+ } else if (looksLikeLength(t)) {
+ decl.width = parseLengthDouble(t);
+ } else if (looksLikeColor(t)) {
+ decl.color = parseColor(t);
+ }
+ }
+ }
+
+ private static void applyWidths(double[] values, BorderDecl top, BorderDecl right, BorderDecl bottom, BorderDecl left) {
+ top.width = values[0];
+ right.width = values[1];
+ bottom.width = values[2];
+ left.width = values[3];
+ }
+
+ private static void applyStyles(String[] values, BorderDecl top, BorderDecl right, BorderDecl bottom, BorderDecl left) {
+ top.style = parseBorderStyle(values[0]);
+ right.style = parseBorderStyle(values[1]);
+ bottom.style = parseBorderStyle(values[2]);
+ left.style = parseBorderStyle(values[3]);
+ }
+
+ private static void applyColors(int[] values, BorderDecl top, BorderDecl right, BorderDecl bottom, BorderDecl left) {
+ top.color = values[0];
+ right.color = values[1];
+ bottom.color = values[2];
+ left.color = values[3];
+ }
+
+ private static CornerRadii applyBorderRadius(String value) {
+ String[] parts = value.split("/");
+ double[] horizontal = normalize1To4Doubles(parseLengthsAsPixelsDouble(parts[0]));
+ if (parts.length == 1) {
+ return CornerRadii.of(horizontal[0], horizontal[1], horizontal[2], horizontal[3]);
+ }
+ double[] vertical = normalize1To4Doubles(parseLengthsAsPixelsDouble(parts[1]));
+ return CornerRadii.of(
+ horizontal[0], vertical[0],
+ horizontal[1], vertical[1],
+ horizontal[2], vertical[2],
+ horizontal[3], vertical[3]
+ );
+ }
+
+ private static double[] parseLengthsAsDouble(String value) {
+ String[] tokens = tokenize(value);
+ double[] values = new double[tokens.length];
+ for (int i = 0; i < tokens.length; i++) {
+ values[i] = parseLengthDouble(tokens[i]);
+ }
+ return values;
+ }
+
+ private static double[] parseLengthsAsPixelsDouble(String value) {
+ String[] tokens = tokenize(value);
+ double[] values = new double[tokens.length];
+ for (int i = 0; i < tokens.length; i++) {
+ values[i] = parseLengthPixelsDouble(tokens[i]);
+ }
+ return values;
+ }
+
+ private static int[] parseLengths(String value) {
+ String[] tokens = tokenize(value);
+ int[] values = new int[tokens.length];
+ for (int i = 0; i < tokens.length; i++) {
+ values[i] = parseLength(tokens[i]);
+ }
+ return values;
+ }
+
+ private static int[] parse1To4Lengths(String value) {
+ return normalize1To4Ints(parseLengths(value));
+ }
+
+ private static double[] parse1To4BorderWidths(String value) {
+ return normalize1To4Doubles(parseLengthsAsDouble(value));
+ }
+
+ private static String[] parse1To4Keywords(String value) {
+ return normalize1To4Strings(tokenize(value));
+ }
+
+ private static int[] parse1To4Colors(String value) {
+ String[] t = tokenize(value);
+ int[] v = new int[t.length];
+ for (int i = 0; i < t.length; i++) {
+ v[i] = parseColor(t[i]);
+ }
+ return normalize1To4Ints(v);
+ }
+
+ private static int[] normalize1To4Ints(int[] in) {
+ if (in.length == 0) {
+ return new int[] {0, 0, 0, 0};
+ }
+ if (in.length == 1) {
+ return new int[] {in[0], in[0], in[0], in[0]};
+ }
+ if (in.length == 2) {
+ return new int[] {in[0], in[1], in[0], in[1]};
+ }
+ if (in.length == 3) {
+ return new int[] {in[0], in[1], in[2], in[1]};
+ }
+ return new int[] {in[0], in[1], in[2], in[3]};
+ }
+
+ private static double[] normalize1To4Doubles(double[] in) {
+ if (in.length == 0) {
+ return new double[] {0, 0, 0, 0};
+ }
+ if (in.length == 1) {
+ return new double[] {in[0], in[0], in[0], in[0]};
+ }
+ if (in.length == 2) {
+ return new double[] {in[0], in[1], in[0], in[1]};
+ }
+ if (in.length == 3) {
+ return new double[] {in[0], in[1], in[2], in[1]};
+ }
+ return new double[] {in[0], in[1], in[2], in[3]};
+ }
+
+ private static String[] normalize1To4Strings(String[] in) {
+ if (in.length == 0) {
+ return new String[] {"none", "none", "none", "none"};
+ }
+ if (in.length == 1) {
+ return new String[] {in[0], in[0], in[0], in[0]};
+ }
+ if (in.length == 2) {
+ return new String[] {in[0], in[1], in[0], in[1]};
+ }
+ if (in.length == 3) {
+ return new String[] {in[0], in[1], in[2], in[1]};
+ }
+ return new String[] {in[0], in[1], in[2], in[3]};
+ }
+
+ private static String[] tokenize(String value) {
+ String s = value.trim();
+ if (s.isEmpty()) {
+ return new String[0];
+ }
+ return s.split("\\s+");
+ }
+
+ private static String firstToken(String value) {
+ String[] t = tokenize(value);
+ return t.length == 0 ? "0" : t[0];
+ }
+
+ private static boolean looksLikeLength(String s) {
+ s = normalize(s);
+ return s.endsWith("px") || isNumber(s);
+ }
+
+ private static boolean looksLikeSignedLength(String s) {
+ s = normalize(s);
+ if (s.endsWith("px")) {
+ s = s.substring(0, s.length() - 2).trim();
+ }
+ return isNumber(s);
+ }
+
+ private static int parseLength(String s) {
+ return (int) Math.round(parseLengthPixelsDouble(s));
+ }
+
+ private static double parseLengthDouble(String s) {
+ s = normalize(s);
+ if (s.endsWith("px")) {
+ s = s.substring(0, s.length() - 2).trim();
+ }
+ if (s.isEmpty()) {
+ return 0;
+ }
+ try {
+ return Math.max(0, Double.parseDouble(s));
+ } catch (NumberFormatException e) {
+ return 0;
+ }
+ }
+
+ private static double parseLengthPixelsDouble(String s) {
+ return UnitsConverter.toPixels(Control.DP + parseLengthDouble(s));
+ }
+
+ private static int parseSignedLength(String s) {
+ s = normalize(s);
+ if (s.endsWith("px")) {
+ s = s.substring(0, s.length() - 2).trim();
+ }
+ if (s.isEmpty()) {
+ return 0;
+ }
+ try {
+ return (int) Math.round(Double.parseDouble(s));
+ } catch (NumberFormatException e) {
+ return 0;
+ }
+ }
+
+ private static double parseSignedLengthDouble(String s) {
+ s = normalize(s);
+ if (s.endsWith("px")) {
+ s = s.substring(0, s.length() - 2).trim();
+ }
+ if (s.isEmpty()) {
+ return 0;
+ }
+ try {
+ return Double.parseDouble(s);
+ } catch (NumberFormatException e) {
+ return 0;
+ }
+ }
+
+ private static boolean isNumber(String s) {
+ try {
+ Double.parseDouble(s);
+ return true;
+ } catch (Exception e) {
+ return false;
+ }
+ }
+
+ private static boolean isBorderStyleToken(String s) {
+ s = normalize(s);
+ return "none".equals(s) || "solid".equals(s) || "dashed".equals(s) || "dotted".equals(s);
+ }
+
+ private static int parseBorderStyle(String s) {
+ s = normalize(s);
+ if ("solid".equals(s)) {
+ return BorderSide.Style.SOLID;
+ }
+ if ("dashed".equals(s)) {
+ return BorderSide.Style.DASHED;
+ }
+ if ("dotted".equals(s)) {
+ return BorderSide.Style.DOTTED;
+ }
+ return BorderSide.Style.NONE;
+ }
+
+ private static int parseOverflow(String value) {
+ String normalized = normalize(value);
+ if ("hidden".equals(normalized)) {
+ return BoxClip.Overflow.HIDDEN;
+ }
+ if ("clip".equals(normalized)) {
+ return BoxClip.Overflow.CLIP;
+ }
+ return BoxClip.Overflow.VISIBLE;
+ }
+
+ private static Elevation parseBoxShadow(String value) {
+ String normalized = normalize(value);
+ if (normalized.length() == 0 || "none".equals(normalized)) {
+ return Elevation.NONE;
+ }
+
+ List layers = splitTopLevelCommaSeparated(value);
+ List parsed = new ArrayList(layers.size());
+ for (int i = 0; i < layers.size(); i++) {
+ Shadow shadow = parseSingleBoxShadow(layers.get(i));
+ if (shadow != Shadow.NONE) {
+ parsed.add(shadow);
+ }
+ }
+
+ if (parsed.isEmpty()) {
+ return Elevation.NONE;
+ }
+ return Elevation.of(parsed.toArray(new Shadow[parsed.size()]));
+ }
+
+ private static Shadow parseSingleBoxShadow(String value) {
+ String[] tokens = tokenize(value);
+ double[] lengths = new double[4];
+ int lengthCount = 0;
+ int alpha = 76;
+ int color = Color.BLACK;
+
+ for (int i = 0; i < tokens.length; i++) {
+ String token = normalize(tokens[i]);
+ if ("inset".equals(token)) {
+ continue;
+ }
+ if (looksLikeColor(token)) {
+ color = parseColor(token);
+ alpha = parseShadowAlpha(token);
+ continue;
+ }
+ if (looksLikeSignedLength(token) && lengthCount < lengths.length) {
+ lengths[lengthCount++] = parseSignedLengthDouble(token);
+ }
+ }
+
+ if (lengthCount < 2) {
+ return Shadow.NONE;
+ }
+
+ double dx = lengths[0];
+ double dy = lengths[1];
+ double blurRadius = lengthCount >= 3 ? Math.max(0, lengths[2]) : 0;
+ double spread = lengthCount >= 4 ? lengths[3] : 0;
+ return Shadow.of(dx, dy, blurRadius, alpha, spread, color);
+ }
+
+ private static List splitTopLevelCommaSeparated(String value) {
+ List items = new ArrayList();
+ StringBuilder current = new StringBuilder();
+ int parenDepth = 0;
+
+ for (int i = 0; i < value.length(); i++) {
+ char ch = value.charAt(i);
+ if (ch == '(') {
+ parenDepth++;
+ } else if (ch == ')') {
+ parenDepth = Math.max(0, parenDepth - 1);
+ }
+
+ if (ch == ',' && parenDepth == 0) {
+ String item = current.toString().trim();
+ if (!item.isEmpty()) {
+ items.add(item);
+ }
+ current.setLength(0);
+ } else {
+ current.append(ch);
+ }
+ }
+
+ String item = current.toString().trim();
+ if (!item.isEmpty()) {
+ items.add(item);
+ }
+ return items;
+ }
+
+ private static Integer tryParseBackgroundColor(String value) {
+ String[] tokens = tokenize(value);
+ for (int i = 0; i < tokens.length; i++) {
+ if (looksLikeColor(tokens[i])) {
+ return Integer.valueOf(parseColor(tokens[i]));
+ }
+ }
+ return null;
+ }
+
+ private static boolean looksLikeColor(String s) {
+ s = normalize(s);
+ return s.startsWith("#") || s.startsWith("rgb(") || s.startsWith("rgba(") || NAMED_COLORS.containsKey(s);
+ }
+
+ private static int parseColor(String s) {
+ s = normalize(s);
+ if (s.startsWith("#")) {
+ return parseHexColor(s);
+ }
+ if (s.startsWith("rgb(") && s.endsWith(")")) {
+ String inner = s.substring(4, s.length() - 1).trim();
+ String[] parts = inner.split(",");
+ if (parts.length == 3) {
+ int r = clamp255(parseInt(parts[0].trim(), 0));
+ int g = clamp255(parseInt(parts[1].trim(), 0));
+ int b = clamp255(parseInt(parts[2].trim(), 0));
+ return (r << 16) | (g << 8) | b;
+ }
+ }
+ if (s.startsWith("rgba(") && s.endsWith(")")) {
+ String inner = s.substring(5, s.length() - 1).trim();
+ String[] parts = inner.split(",");
+ if (parts.length >= 3) {
+ int r = clamp255(parseInt(parts[0].trim(), 0));
+ int g = clamp255(parseInt(parts[1].trim(), 0));
+ int b = clamp255(parseInt(parts[2].trim(), 0));
+ return (r << 16) | (g << 8) | b;
+ }
+ }
+ Integer named = NAMED_COLORS.get(s);
+ return named != null ? named.intValue() : 0;
+ }
+
+ private static int parseShadowAlpha(String s) {
+ s = normalize(s);
+ if (s.startsWith("rgba(") && s.endsWith(")")) {
+ String inner = s.substring(5, s.length() - 1).trim();
+ String[] parts = inner.split(",");
+ if (parts.length == 4) {
+ try {
+ double alpha = Double.parseDouble(parts[3].trim());
+ return clamp255((int) Math.round(alpha * 255));
+ } catch (Exception e) {
+ return 76;
+ }
+ }
+ }
+ if (s.startsWith("#")) {
+ String hex = s.substring(1).trim();
+ if (hex.length() == 4) {
+ return Integer.parseInt("" + hex.charAt(0) + hex.charAt(0), 16);
+ }
+ if (hex.length() == 8) {
+ return Integer.parseInt(hex.substring(0, 2), 16);
+ }
+ }
+ return 76;
+ }
+
+ private static int parseHexColor(String s) {
+ String hex = s.substring(1).trim();
+ if (hex.length() == 3) {
+ int r = Integer.parseInt("" + hex.charAt(0) + hex.charAt(0), 16);
+ int g = Integer.parseInt("" + hex.charAt(1) + hex.charAt(1), 16);
+ int b = Integer.parseInt("" + hex.charAt(2) + hex.charAt(2), 16);
+ return (r << 16) | (g << 8) | b;
+ }
+ if (hex.length() == 6) {
+ return Integer.parseInt(hex, 16);
+ }
+ if (hex.length() == 8) {
+ return Integer.parseInt(hex.substring(2), 16);
+ }
+ return 0;
+ }
+
+ private static int parseInt(String s, int def) {
+ try {
+ return Integer.parseInt(s);
+ } catch (Exception e) {
+ return def;
+ }
+ }
+
+ private static int clamp255(int v) {
+ return v < 0 ? 0 : (v > 255 ? 255 : v);
+ }
+
+ private static void copyDecl(BorderDecl src, BorderDecl dst) {
+ dst.width = src.width;
+ dst.style = src.style;
+ dst.color = src.color;
+ dst.align = src.align;
+ }
+
+ private static final class BorderDecl {
+ double width = 0;
+ int style = BorderSide.Style.NONE;
+ int color = 0;
+ int align = BorderSide.Align.INSIDE;
+
+ static BorderDecl from(BorderSide side) {
+ BorderDecl decl = new BorderDecl();
+ if (side != null) {
+ decl.width = Settings.screenDensity == 0 ? side.width : side.width / Settings.screenDensity;
+ decl.style = side.style;
+ decl.color = side.color;
+ decl.align = side.align;
+ }
+ return decl;
+ }
+
+ BorderSide toBorderSide() {
+ return BorderSide.with(width, style, color, align);
+ }
+ }
+
+ private static final Map NAMED_COLORS = createNamedColors();
+
+ private static Map createNamedColors() {
+ Map map = new HashMap();
+ map.put("black", Integer.valueOf(Color.BLACK));
+ map.put("white", Integer.valueOf(Color.WHITE));
+ map.put("red", Integer.valueOf(Color.RED));
+ map.put("green", Integer.valueOf(0x008000));
+ map.put("blue", Integer.valueOf(Color.BLUE));
+ map.put("yellow", Integer.valueOf(Color.YELLOW));
+ map.put("orange", Integer.valueOf(0xFFA500));
+ map.put("gray", Integer.valueOf(0x808080));
+ map.put("grey", Integer.valueOf(0x808080));
+ map.put("silver", Integer.valueOf(0xC0C0C0));
+ map.put("maroon", Integer.valueOf(0x800000));
+ map.put("purple", Integer.valueOf(0x800080));
+ map.put("fuchsia", Integer.valueOf(0xFF00FF));
+ map.put("lime", Integer.valueOf(0x00FF00));
+ map.put("olive", Integer.valueOf(0x808000));
+ map.put("navy", Integer.valueOf(0x000080));
+ map.put("teal", Integer.valueOf(0x008080));
+ map.put("aqua", Integer.valueOf(0x00FFFF));
+ map.put("transparent", Integer.valueOf(0x000000));
+ return map;
+ }
+}
diff --git a/TotalCrossSDK/src/main/java/totalcross/ui/style/geom/BoxGeometry.java b/TotalCrossSDK/src/main/java/totalcross/ui/style/geom/BoxGeometry.java
new file mode 100644
index 000000000..9fa09e530
--- /dev/null
+++ b/TotalCrossSDK/src/main/java/totalcross/ui/style/geom/BoxGeometry.java
@@ -0,0 +1,149 @@
+// Copyright (C) 2026 Amalgam Solucoes em TI Ltda
+//
+// SPDX-License-Identifier: LGPL-2.1-only
+
+package totalcross.ui.style.geom;
+
+import totalcross.ui.Insets;
+import totalcross.ui.gfx.RRect;
+import totalcross.ui.gfx.Rect;
+import totalcross.ui.style.model.BorderSide;
+import totalcross.ui.style.model.BoxBorder;
+import totalcross.ui.style.model.BoxLayout;
+import totalcross.ui.style.model.BoxPaint;
+import totalcross.ui.style.model.BoxShape;
+import totalcross.ui.style.model.BoxStyle;
+import totalcross.ui.style.model.CornerRadii;
+
+/**
+ * Resolves the concrete rectangles and corner radii used to paint a box.
+ */
+public final class BoxGeometry {
+ public final Rect borderBox = new Rect();
+ public final Rect paddingBox = new Rect();
+ public final Rect contentBox = new Rect();
+
+ public CornerRadii borderRadii = CornerRadii.ZERO;
+ public CornerRadii paddingRadii = CornerRadii.ZERO;
+ public CornerRadii contentRadii = CornerRadii.ZERO;
+
+ /**
+ * Returns the border box as a rounded rectangle.
+ */
+ public RRect borderRRect() {
+ return new RRect(
+ borderBox.x,
+ borderBox.y,
+ borderBox.width,
+ borderBox.height,
+ new double[] {
+ borderRadii.topLeftX, borderRadii.topLeftY,
+ borderRadii.topRightX, borderRadii.topRightY,
+ borderRadii.bottomRightX, borderRadii.bottomRightY,
+ borderRadii.bottomLeftX, borderRadii.bottomLeftY
+ });
+ }
+
+ /**
+ * Returns the padding box as a rounded rectangle.
+ */
+ public RRect paddingRRect() {
+ return new RRect(
+ paddingBox.x,
+ paddingBox.y,
+ paddingBox.width,
+ paddingBox.height,
+ new double[] {
+ paddingRadii.topLeftX, paddingRadii.topLeftY,
+ paddingRadii.topRightX, paddingRadii.topRightY,
+ paddingRadii.bottomRightX, paddingRadii.bottomRightY,
+ paddingRadii.bottomLeftX, paddingRadii.bottomLeftY
+ });
+ }
+
+ /**
+ * Returns the content box as a rounded rectangle.
+ */
+ public RRect contentRRect() {
+ return new RRect(
+ contentBox.x,
+ contentBox.y,
+ contentBox.width,
+ contentBox.height,
+ new double[] {
+ contentRadii.topLeftX, contentRadii.topLeftY,
+ contentRadii.topRightX, contentRadii.topRightY,
+ contentRadii.bottomRightX, contentRadii.bottomRightY,
+ contentRadii.bottomLeftX, contentRadii.bottomLeftY
+ });
+ }
+
+ /**
+ * Computes box geometry for the given bounds and style.
+ */
+ public static BoxGeometry compute(int x, int y, int w, int h, BoxStyle style) {
+ BoxLayout layout = style == null ? null : style.layout;
+ BoxPaint paint = style == null ? null : style.paint;
+ BoxShape shape = style == null ? null : style.shape;
+ BoxGeometry g = new BoxGeometry();
+ int safeW = Math.max(0, w);
+ int safeH = Math.max(0, h);
+ Insets padding = layout == null ? null : layout.padding;
+ BoxBorder border = paint == null ? null : paint.border;
+ Insets borderInsets = border == null ? new Insets() : border.getInsets();
+ int align = resolveStrokeAlign(border);
+
+ int borderBoxX = x;
+ int borderBoxY = y;
+ int borderBoxW = safeW;
+ int borderBoxH = safeH;
+
+ if (align == BorderSide.Align.CENTER) {
+ borderBoxX -= borderInsets.left / 2;
+ borderBoxY -= borderInsets.top / 2;
+ borderBoxW += (borderInsets.left + borderInsets.right) / 2;
+ borderBoxH += (borderInsets.top + borderInsets.bottom) / 2;
+ } else if (align == BorderSide.Align.OUTSIDE) {
+ borderBoxX -= borderInsets.left;
+ borderBoxY -= borderInsets.top;
+ borderBoxW += borderInsets.left + borderInsets.right;
+ borderBoxH += borderInsets.top + borderInsets.bottom;
+ }
+
+ g.borderBox.set(borderBoxX, borderBoxY, Math.max(0, borderBoxW), Math.max(0, borderBoxH));
+
+ g.borderRadii = (shape == null ? CornerRadii.ZERO : shape.radii)
+ .clamp(g.borderBox.width, g.borderBox.height);
+
+ insetRect(g.borderBox, borderInsets, g.paddingBox);
+ g.paddingRadii = g.borderRadii
+ .inset(borderInsets)
+ .clamp(g.paddingBox.width, g.paddingBox.height);
+
+ Insets safePadding = padding == null ? new Insets() : padding;
+ insetRect(g.paddingBox, safePadding, g.contentBox);
+ g.contentRadii = g.paddingRadii
+ .inset(safePadding)
+ .clamp(g.contentBox.width, g.contentBox.height);
+
+ return g;
+ }
+
+ /**
+ * Insets the source rectangle into the destination rectangle.
+ */
+ public static void insetRect(Rect src, Insets insets, Rect dst) {
+ int x = src.x + insets.left;
+ int y = src.y + insets.top;
+ int w = src.width - insets.left - insets.right;
+ int h = src.height - insets.top - insets.bottom;
+ dst.set(x, y, Math.max(0, w), Math.max(0, h));
+ }
+
+ private static int resolveStrokeAlign(BoxBorder border) {
+ if (border == null || border.top == null) {
+ return BorderSide.Align.INSIDE;
+ }
+ return border.top.align;
+ }
+}
diff --git a/TotalCrossSDK/src/main/java/totalcross/ui/style/model/BorderSide.java b/TotalCrossSDK/src/main/java/totalcross/ui/style/model/BorderSide.java
new file mode 100644
index 000000000..a55d68641
--- /dev/null
+++ b/TotalCrossSDK/src/main/java/totalcross/ui/style/model/BorderSide.java
@@ -0,0 +1,109 @@
+// Copyright (C) 2026 Amalgam Solucoes em TI Ltda
+//
+// SPDX-License-Identifier: LGPL-2.1-only
+
+package totalcross.ui.style.model;
+
+import totalcross.ui.Control;
+import totalcross.util.UnitsConverter;
+
+/**
+ * Describes a single side of a box border.
+ */
+public final class BorderSide {
+ /**
+ * Supported border styles.
+ */
+ public static final class Style {
+ public static final int NONE = 0;
+ public static final int SOLID = 1;
+ public static final int DASHED = 2;
+ public static final int DOTTED = 3;
+
+ private Style() {
+ }
+ }
+
+ /**
+ * Supported border alignment modes.
+ */
+ public static final class Align {
+ public static final int INSIDE = 0;
+ public static final int CENTER = 1;
+ public static final int OUTSIDE = 2;
+
+ private Align() {
+ }
+ }
+
+ public final double width;
+ public final int style;
+ public final int color;
+
+ public final int align;
+
+ /** Constant for an invisible border side. */
+ public static final BorderSide NONE = with(0, Style.NONE, 0);
+
+ private BorderSide(double width, int style, int color, int align) {
+ validateStyle(style);
+ validateAlign(align);
+ this.width = UnitsConverter.toPixels(Control.DP + width);
+ this.style = style;
+ this.color = color;
+ this.align = align;
+ }
+
+ /**
+ * Creates a border side using inside alignment.
+ */
+ public static BorderSide with(double width, int style, int color) {
+ return with(width, style, color, Align.INSIDE);
+ }
+
+ /**
+ * Creates a border side.
+ */
+ public static BorderSide with(double width, int style, int color, int align) {
+ return new BorderSide(width, style, color, align);
+ }
+
+ /**
+ * Returns whether this side should be painted.
+ */
+ public boolean isVisible() {
+ return style != Style.NONE && width > 0;
+ }
+
+ private static void validateStyle(int style) {
+ if (!isValidStyle(style)) {
+ throw new IllegalArgumentException("Invalid BorderSide.Style: " + style);
+ }
+ }
+
+ private static void validateAlign(int align) {
+ if (!isValidAlign(align)) {
+ throw new IllegalArgumentException("Invalid BorderSide.Align: " + align);
+ }
+ }
+
+ /**
+ * Returns whether the given value is a valid border style.
+ */
+ public static boolean isValidStyle(int style) {
+ return style == Style.NONE
+ || style == Style.SOLID
+ || style == Style.DASHED
+ || style == Style.DOTTED;
+ }
+
+ /**
+ * Returns whether the given value is a valid border alignment.
+ */
+ public static boolean isValidAlign(int align) {
+ return align == Align.INSIDE
+ || align == Align.CENTER
+ || align == Align.OUTSIDE;
+ }
+
+}
diff --git a/TotalCrossSDK/src/main/java/totalcross/ui/style/model/BoxBorder.java b/TotalCrossSDK/src/main/java/totalcross/ui/style/model/BoxBorder.java
new file mode 100644
index 000000000..f0b4dda6c
--- /dev/null
+++ b/TotalCrossSDK/src/main/java/totalcross/ui/style/model/BoxBorder.java
@@ -0,0 +1,118 @@
+// Copyright (C) 2026 Amalgam Solucoes em TI Ltda
+//
+// SPDX-License-Identifier: LGPL-2.1-only
+
+package totalcross.ui.style.model;
+
+import java.util.Objects;
+
+import totalcross.ui.Insets;
+
+/**
+ * Describes the border of a box, including all four sides.
+ */
+public final class BoxBorder {
+ public final BorderSide top;
+ public final BorderSide right;
+ public final BorderSide bottom;
+ public final BorderSide left;
+
+ private BoxBorder(
+ BorderSide top,
+ BorderSide right,
+ BorderSide bottom,
+ BorderSide left) {
+ this.top = Objects.requireNonNull(top, "BoxBorder.top cannot be null");
+ this.right = Objects.requireNonNull(right, "BoxBorder.right cannot be null");
+ this.bottom = Objects.requireNonNull(bottom, "BoxBorder.bottom cannot be null");
+ this.left = Objects.requireNonNull(left, "BoxBorder.left cannot be null");
+ }
+
+ /**
+ * Creates a builder for a box border.
+ */
+ public static Builder builder() {
+ return new Builder();
+ }
+
+ /**
+ * Returns the border widths as insets.
+ */
+ public Insets getInsets() {
+ return new Insets(
+ (int) Math.round(top.width),
+ (int) Math.round(left.width),
+ (int) Math.round(bottom.width),
+ (int) Math.round(right.width));
+ }
+
+ /**
+ * Returns whether at least one side should be painted.
+ */
+ public boolean isVisible() {
+ return top.isVisible() || right.isVisible()
+ || bottom.isVisible() || left.isVisible();
+ }
+
+ /**
+ * Builder for {@link BoxBorder}.
+ */
+ public static final class Builder {
+ private BorderSide top = BorderSide.NONE;
+ private BorderSide right = BorderSide.NONE;
+ private BorderSide bottom = BorderSide.NONE;
+ private BorderSide left = BorderSide.NONE;
+
+ private Builder() {
+ }
+
+ /**
+ * Sets all sides to the same border side.
+ */
+ public Builder all(BorderSide side) {
+ return top(side)
+ .right(side)
+ .bottom(side)
+ .left(side);
+ }
+
+ /**
+ * Sets the top border side.
+ */
+ public Builder top(BorderSide top) {
+ this.top = Objects.requireNonNull(top, "BoxBorder.Builder.top cannot be null");
+ return this;
+ }
+
+ /**
+ * Sets the right border side.
+ */
+ public Builder right(BorderSide right) {
+ this.right = Objects.requireNonNull(right, "BoxBorder.Builder.right cannot be null");
+ return this;
+ }
+
+ /**
+ * Sets the bottom border side.
+ */
+ public Builder bottom(BorderSide bottom) {
+ this.bottom = Objects.requireNonNull(bottom, "BoxBorder.Builder.bottom cannot be null");
+ return this;
+ }
+
+ /**
+ * Sets the left border side.
+ */
+ public Builder left(BorderSide left) {
+ this.left = Objects.requireNonNull(left, "BoxBorder.Builder.left cannot be null");
+ return this;
+ }
+
+ /**
+ * Builds the immutable border.
+ */
+ public BoxBorder build() {
+ return new BoxBorder(top, right, bottom, left);
+ }
+ }
+}
diff --git a/TotalCrossSDK/src/main/java/totalcross/ui/style/model/BoxClip.java b/TotalCrossSDK/src/main/java/totalcross/ui/style/model/BoxClip.java
new file mode 100644
index 000000000..f64700579
--- /dev/null
+++ b/TotalCrossSDK/src/main/java/totalcross/ui/style/model/BoxClip.java
@@ -0,0 +1,91 @@
+// Copyright (C) 2026 Amalgam Solucoes em TI Ltda
+//
+// SPDX-License-Identifier: LGPL-2.1-only
+
+package totalcross.ui.style.model;
+
+/**
+ * Describes overflow clipping behavior for a box.
+ */
+public final class BoxClip {
+ /**
+ * Supported overflow modes.
+ */
+ public static final class Overflow {
+ public static final int VISIBLE = 0;
+ public static final int HIDDEN = 1;
+ public static final int CLIP = 2;
+
+ private Overflow() {
+ }
+ }
+
+ public final int overflow;
+
+ /**
+ * Creates a clip configuration with visible overflow.
+ */
+ public BoxClip() {
+ this(Overflow.VISIBLE);
+ }
+
+ private BoxClip(int overflow) {
+ validateOverflow(overflow);
+ this.overflow = overflow;
+ }
+
+ /**
+ * Creates a builder for a clip configuration.
+ */
+ public static Builder builder() {
+ return new Builder();
+ }
+
+ /**
+ * Returns whether overflow should be clipped.
+ */
+ public boolean shouldClip() {
+ return overflow == Overflow.HIDDEN || overflow == Overflow.CLIP;
+ }
+
+ /**
+ * Returns whether the given value is a valid overflow mode.
+ */
+ public static boolean isValidOverflow(int overflow) {
+ return overflow == Overflow.VISIBLE
+ || overflow == Overflow.HIDDEN
+ || overflow == Overflow.CLIP;
+ }
+
+ private static void validateOverflow(int overflow) {
+ if (!isValidOverflow(overflow)) {
+ throw new IllegalArgumentException("Invalid BoxClip overflow: " + overflow);
+ }
+ }
+
+ /**
+ * Builder for {@link BoxClip}.
+ */
+ public static final class Builder {
+ private int overflow = Overflow.VISIBLE;
+
+ private Builder() {
+ }
+
+ /**
+ * Sets the overflow mode.
+ */
+ public Builder overflow(int overflow) {
+ validateOverflow(overflow);
+ this.overflow = overflow;
+ return this;
+ }
+
+ /**
+ * Builds the immutable clip configuration.
+ */
+ public BoxClip build() {
+ return new BoxClip(overflow);
+ }
+ }
+}
diff --git a/TotalCrossSDK/src/main/java/totalcross/ui/style/model/BoxLayout.java b/TotalCrossSDK/src/main/java/totalcross/ui/style/model/BoxLayout.java
new file mode 100644
index 000000000..877cbb37d
--- /dev/null
+++ b/TotalCrossSDK/src/main/java/totalcross/ui/style/model/BoxLayout.java
@@ -0,0 +1,100 @@
+// Copyright (C) 2026 Amalgam Solucoes em TI Ltda
+//
+// SPDX-License-Identifier: LGPL-2.1-only
+
+package totalcross.ui.style.model;
+
+import java.util.Objects;
+
+import totalcross.ui.Insets;
+
+/**
+ * Describes box layout properties such as padding.
+ */
+public final class BoxLayout {
+ public final Insets padding;
+
+ /**
+ * Creates a layout with zero padding.
+ */
+ public BoxLayout() {
+ this(new Insets());
+ }
+
+ private BoxLayout(Insets padding) {
+ Objects.requireNonNull(padding, "BoxLayout.padding cannot be null");
+ validateNonNegative("top", padding.top);
+ validateNonNegative("right", padding.right);
+ validateNonNegative("bottom", padding.bottom);
+ validateNonNegative("left", padding.left);
+ this.padding = new Insets(padding.top, padding.left, padding.bottom, padding.right);
+ }
+
+ /**
+ * Creates a builder for {@link BoxLayout}.
+ */
+ public static Builder builder() {
+ return new Builder();
+ }
+
+ private static void validateNonNegative(String edge, int value) {
+ if (value < 0) {
+ throw new IllegalArgumentException("BoxLayout padding " + edge + " must be >= 0: " + value);
+ }
+ }
+
+ /**
+ * Builder for {@link BoxLayout}.
+ */
+ public static final class Builder {
+ private Insets padding = new Insets();
+
+ private Builder() {
+ }
+
+ /**
+ * Sets the same padding on all sides.
+ */
+ public Builder padding(int all) {
+ return padding(all, all, all, all);
+ }
+
+ /**
+ * Sets vertical and horizontal padding.
+ */
+ public Builder padding(int vertical, int horizontal) {
+ return padding(vertical, horizontal, vertical, horizontal);
+ }
+
+ /**
+ * Sets padding for each side.
+ */
+ public Builder padding(int top, int right, int bottom, int left) {
+ validateNonNegative("top", top);
+ validateNonNegative("right", right);
+ validateNonNegative("bottom", bottom);
+ validateNonNegative("left", left);
+ padding = new Insets(top, left, bottom, right);
+ return this;
+ }
+
+ /**
+ * Copies padding from an existing {@link Insets} instance.
+ */
+ public Builder padding(Insets padding) {
+ this.padding = Objects.requireNonNull(padding, "BoxLayout.Builder.padding cannot be null");
+ validateNonNegative("top", padding.top);
+ validateNonNegative("right", padding.right);
+ validateNonNegative("bottom", padding.bottom);
+ validateNonNegative("left", padding.left);
+ return this;
+ }
+
+ /**
+ * Builds the immutable layout object.
+ */
+ public BoxLayout build() {
+ return new BoxLayout(padding);
+ }
+ }
+}
diff --git a/TotalCrossSDK/src/main/java/totalcross/ui/style/model/BoxPaint.java b/TotalCrossSDK/src/main/java/totalcross/ui/style/model/BoxPaint.java
new file mode 100644
index 000000000..4d4ee6016
--- /dev/null
+++ b/TotalCrossSDK/src/main/java/totalcross/ui/style/model/BoxPaint.java
@@ -0,0 +1,79 @@
+// Copyright (C) 2026 Amalgam Solucoes em TI Ltda
+//
+// SPDX-License-Identifier: LGPL-2.1-only
+
+package totalcross.ui.style.model;
+
+import java.util.Objects;
+
+/**
+ * Describes paint properties for a box, including background and border.
+ */
+public final class BoxPaint {
+ public final int backgroundColor;
+ public final int pressedColor;
+ public final BoxBorder border;
+
+ /**
+ * Creates a paint configuration with no background and no visible border.
+ */
+ public BoxPaint() {
+ this(0, 0, BoxBorder.builder().all(BorderSide.NONE).build());
+ }
+
+ private BoxPaint(int backgroundColor, int pressedColor, BoxBorder border) {
+ this.backgroundColor = backgroundColor;
+ this.pressedColor = pressedColor;
+ this.border = Objects.requireNonNull(border, "BoxPaint.border cannot be null");
+ }
+
+ /**
+ * Creates a builder for {@link BoxPaint}.
+ */
+ public static Builder builder() {
+ return new Builder();
+ }
+
+ /**
+ * Builder for {@link BoxPaint}.
+ */
+ public static final class Builder {
+ private int backgroundColor;
+ private int pressedColor;
+ private BoxBorder border = BoxBorder.builder().all(BorderSide.NONE).build();
+
+ private Builder() {
+ }
+
+ /**
+ * Sets the normal background color.
+ */
+ public Builder backgroundColor(int backgroundColor) {
+ this.backgroundColor = backgroundColor;
+ return this;
+ }
+
+ /**
+ * Sets the pressed-state background color.
+ */
+ public Builder pressedColor(int pressedColor) {
+ this.pressedColor = pressedColor;
+ return this;
+ }
+
+ /**
+ * Sets the border configuration.
+ */
+ public Builder border(BoxBorder border) {
+ this.border = Objects.requireNonNull(border, "BoxPaint.Builder.border cannot be null");
+ return this;
+ }
+
+ /**
+ * Builds the immutable paint configuration.
+ */
+ public BoxPaint build() {
+ return new BoxPaint(backgroundColor, pressedColor, border);
+ }
+ }
+}
diff --git a/TotalCrossSDK/src/main/java/totalcross/ui/style/model/BoxShape.java b/TotalCrossSDK/src/main/java/totalcross/ui/style/model/BoxShape.java
new file mode 100644
index 000000000..d50addf3b
--- /dev/null
+++ b/TotalCrossSDK/src/main/java/totalcross/ui/style/model/BoxShape.java
@@ -0,0 +1,73 @@
+// Copyright (C) 2026 Amalgam Solucoes em TI Ltda
+//
+// SPDX-License-Identifier: LGPL-2.1-only
+
+package totalcross.ui.style.model;
+
+import java.util.Objects;
+
+/**
+ * Describes the geometric shape of a box.
+ */
+public final class BoxShape {
+ public final CornerRadii radii;
+
+ /**
+ * Creates a shape with zero radii.
+ */
+ public BoxShape() {
+ this(CornerRadii.ZERO);
+ }
+
+ private BoxShape(CornerRadii radii) {
+ this.radii = Objects.requireNonNull(radii, "BoxShape.radii cannot be null");
+ }
+
+ /**
+ * Creates a builder for {@link BoxShape}.
+ */
+ public static Builder builder() {
+ return new Builder();
+ }
+
+ /**
+ * Builder for {@link BoxShape}.
+ */
+ public static final class Builder {
+ private CornerRadii radii = CornerRadii.ZERO;
+
+ private Builder() {
+ }
+
+ /**
+ * Sets the same radius for all corners.
+ */
+ public Builder radius(double all) {
+ radii = CornerRadii.all(all);
+ return this;
+ }
+
+ /**
+ * Sets one radius per corner, using the same value for x and y.
+ */
+ public Builder radius(double topLeft, double topRight, double bottomRight, double bottomLeft) {
+ radii = CornerRadii.of(topLeft, topRight, bottomRight, bottomLeft);
+ return this;
+ }
+
+ /**
+ * Sets the full corner radii object.
+ */
+ public Builder radii(CornerRadii radii) {
+ this.radii = Objects.requireNonNull(radii, "BoxShape.Builder.radii cannot be null");
+ return this;
+ }
+
+ /**
+ * Builds the immutable shape.
+ */
+ public BoxShape build() {
+ return new BoxShape(radii);
+ }
+ }
+}
diff --git a/TotalCrossSDK/src/main/java/totalcross/ui/style/model/BoxStyle.java b/TotalCrossSDK/src/main/java/totalcross/ui/style/model/BoxStyle.java
new file mode 100644
index 000000000..3f184d8db
--- /dev/null
+++ b/TotalCrossSDK/src/main/java/totalcross/ui/style/model/BoxStyle.java
@@ -0,0 +1,112 @@
+// Copyright (C) 2026 Amalgam Solucoes em TI Ltda
+//
+// SPDX-License-Identifier: LGPL-2.1-only
+
+package totalcross.ui.style.model;
+
+import java.util.Objects;
+
+/**
+ * Aggregates all style properties used to paint and clip a box.
+ */
+public final class BoxStyle {
+ public final BoxLayout layout;
+ public final BoxShape shape;
+ public final BoxPaint paint;
+ public final BoxClip clip;
+ public final Elevation elevation;
+
+ /**
+ * Creates a default box style.
+ */
+ public BoxStyle() {
+ this(new BoxLayout(), new BoxShape(), new BoxPaint(), new BoxClip(), Elevation.NONE);
+ }
+
+ /**
+ * Creates a box style with explicit values for all parts.
+ */
+ public BoxStyle(BoxLayout layout, BoxShape shape, BoxPaint paint, BoxClip clip, Elevation elevation) {
+ this.layout = Objects.requireNonNull(layout, "BoxStyle.layout cannot be null");
+ this.shape = Objects.requireNonNull(shape, "BoxStyle.shape cannot be null");
+ this.paint = Objects.requireNonNull(paint, "BoxStyle.paint cannot be null");
+ this.clip = Objects.requireNonNull(clip, "BoxStyle.clip cannot be null");
+ this.elevation = Objects.requireNonNull(elevation, "BoxStyle.elevation cannot be null");
+ }
+
+ /**
+ * Creates a builder for {@link BoxStyle}.
+ */
+ public static Builder builder() {
+ return new Builder();
+ }
+
+ /**
+ * Builder for {@link BoxStyle}.
+ */
+ public static final class Builder {
+ private BoxLayout layout = new BoxLayout();
+ private BoxShape shape = new BoxShape();
+ private BoxPaint paint = new BoxPaint();
+ private BoxClip clip = new BoxClip();
+ private Elevation elevation = Elevation.NONE;
+
+ private Builder() {
+ }
+
+ /**
+ * Sets layout properties.
+ */
+ public Builder layout(BoxLayout layout) {
+ this.layout = Objects.requireNonNull(layout, "BoxStyle.Builder.layout cannot be null");
+ return this;
+ }
+
+ /**
+ * Sets shape properties.
+ */
+ public Builder shape(BoxShape shape) {
+ this.shape = Objects.requireNonNull(shape, "BoxStyle.Builder.shape cannot be null");
+ return this;
+ }
+
+ /**
+ * Sets paint properties.
+ */
+ public Builder paint(BoxPaint paint) {
+ this.paint = Objects.requireNonNull(paint, "BoxStyle.Builder.paint cannot be null");
+ return this;
+ }
+
+ /**
+ * Sets clip properties.
+ */
+ public Builder clip(BoxClip clip) {
+ this.clip = Objects.requireNonNull(clip, "BoxStyle.Builder.clip cannot be null");
+ return this;
+ }
+
+ /**
+ * Sets elevation properties.
+ */
+ public Builder elevation(Elevation elevation) {
+ this.elevation = Objects.requireNonNull(elevation, "BoxStyle.Builder.elevation cannot be null");
+ return this;
+ }
+
+ /**
+ * Sets elevation from a list of shadows.
+ */
+ public Builder shadows(Shadow... shadows) {
+ this.elevation = Elevation.of(shadows);
+ return this;
+ }
+
+ /**
+ * Builds the immutable box style.
+ */
+ public BoxStyle build() {
+ return new BoxStyle(layout, shape, paint, clip, elevation);
+ }
+ }
+}
diff --git a/TotalCrossSDK/src/main/java/totalcross/ui/style/model/CornerRadii.java b/TotalCrossSDK/src/main/java/totalcross/ui/style/model/CornerRadii.java
new file mode 100644
index 000000000..483bdd684
--- /dev/null
+++ b/TotalCrossSDK/src/main/java/totalcross/ui/style/model/CornerRadii.java
@@ -0,0 +1,165 @@
+// Copyright (C) 2026 Amalgam Solucoes em TI Ltda
+//
+// SPDX-License-Identifier: LGPL-2.1-only
+
+package totalcross.ui.style.model;
+
+import totalcross.ui.Insets;
+
+/**
+ * Represents the x and y radii for each corner of a box.
+ */
+public final class CornerRadii {
+ /** Constant for zero radii on all corners. */
+ public static final CornerRadii ZERO = new CornerRadii(0, 0, 0, 0, 0, 0, 0, 0);
+
+ public final double topLeftX;
+ public final double topLeftY;
+ public final double topRightX;
+ public final double topRightY;
+ public final double bottomRightX;
+ public final double bottomRightY;
+ public final double bottomLeftX;
+ public final double bottomLeftY;
+
+ private CornerRadii(
+ double topLeftX,
+ double topLeftY,
+ double topRightX,
+ double topRightY,
+ double bottomRightX,
+ double bottomRightY,
+ double bottomLeftX,
+ double bottomLeftY
+ ) {
+ validateNonNegative("topLeftX", topLeftX);
+ validateNonNegative("topLeftY", topLeftY);
+ validateNonNegative("topRightX", topRightX);
+ validateNonNegative("topRightY", topRightY);
+ validateNonNegative("bottomRightX", bottomRightX);
+ validateNonNegative("bottomRightY", bottomRightY);
+ validateNonNegative("bottomLeftX", bottomLeftX);
+ validateNonNegative("bottomLeftY", bottomLeftY);
+ this.topLeftX = topLeftX;
+ this.topLeftY = topLeftY;
+ this.topRightX = topRightX;
+ this.topRightY = topRightY;
+ this.bottomRightX = bottomRightX;
+ this.bottomRightY = bottomRightY;
+ this.bottomLeftX = bottomLeftX;
+ this.bottomLeftY = bottomLeftY;
+ }
+
+ /**
+ * Creates radii with the same value on all corners.
+ */
+ public static CornerRadii all(double value) {
+ return of(value, value, value, value);
+ }
+
+ /**
+ * Creates radii with one value per corner, using the same value for x and y.
+ */
+ public static CornerRadii of(double topLeft, double topRight, double bottomRight, double bottomLeft) {
+ return of(
+ topLeft, topLeft,
+ topRight, topRight,
+ bottomRight, bottomRight,
+ bottomLeft, bottomLeft
+ );
+ }
+
+ /**
+ * Creates radii with explicit x and y values for each corner.
+ */
+ public static CornerRadii of(
+ double topLeftX,
+ double topLeftY,
+ double topRightX,
+ double topRightY,
+ double bottomRightX,
+ double bottomRightY,
+ double bottomLeftX,
+ double bottomLeftY
+ ) {
+ if (topLeftX == 0 && topLeftY == 0
+ && topRightX == 0 && topRightY == 0
+ && bottomRightX == 0 && bottomRightY == 0
+ && bottomLeftX == 0 && bottomLeftY == 0) {
+ return ZERO;
+ }
+ return new CornerRadii(
+ topLeftX, topLeftY,
+ topRightX, topRightY,
+ bottomRightX, bottomRightY,
+ bottomLeftX, bottomLeftY
+ );
+ }
+
+ /**
+ * Returns whether all radii are zero.
+ */
+ public boolean isZero() {
+ return this == ZERO
+ || (topLeftX == 0 && topLeftY == 0
+ && topRightX == 0 && topRightY == 0
+ && bottomRightX == 0 && bottomRightY == 0
+ && bottomLeftX == 0 && bottomLeftY == 0);
+ }
+
+ /**
+ * Returns a copy with the given delta applied to all radii, clamped at zero.
+ */
+ public CornerRadii offset(double delta) {
+ return of(
+ Math.max(0, topLeftX + delta),
+ Math.max(0, topLeftY + delta),
+ Math.max(0, topRightX + delta),
+ Math.max(0, topRightY + delta),
+ Math.max(0, bottomRightX + delta),
+ Math.max(0, bottomRightY + delta),
+ Math.max(0, bottomLeftX + delta),
+ Math.max(0, bottomLeftY + delta)
+ );
+ }
+
+ /**
+ * Returns a copy inset by the given edge insets.
+ */
+ public CornerRadii inset(Insets insets) {
+ return of(
+ Math.max(0, topLeftX - insets.left),
+ Math.max(0, topLeftY - insets.top),
+ Math.max(0, topRightX - insets.right),
+ Math.max(0, topRightY - insets.top),
+ Math.max(0, bottomRightX - insets.right),
+ Math.max(0, bottomRightY - insets.bottom),
+ Math.max(0, bottomLeftX - insets.left),
+ Math.max(0, bottomLeftY - insets.bottom)
+ );
+ }
+
+ /**
+ * Returns a copy clamped to fit inside the given dimensions.
+ */
+ public CornerRadii clamp(int w, int h) {
+ double maxX = Math.max(0, w / 2.0);
+ double maxY = Math.max(0, h / 2.0);
+ return of(
+ Math.min(topLeftX, maxX),
+ Math.min(topLeftY, maxY),
+ Math.min(topRightX, maxX),
+ Math.min(topRightY, maxY),
+ Math.min(bottomRightX, maxX),
+ Math.min(bottomRightY, maxY),
+ Math.min(bottomLeftX, maxX),
+ Math.min(bottomLeftY, maxY)
+ );
+ }
+
+ private static void validateNonNegative(String name, double value) {
+ if (value < 0) {
+ throw new IllegalArgumentException("CornerRadii " + name + " must be >= 0: " + value);
+ }
+ }
+}
diff --git a/TotalCrossSDK/src/main/java/totalcross/ui/style/model/Elevation.java b/TotalCrossSDK/src/main/java/totalcross/ui/style/model/Elevation.java
new file mode 100644
index 000000000..f1665a5c6
--- /dev/null
+++ b/TotalCrossSDK/src/main/java/totalcross/ui/style/model/Elevation.java
@@ -0,0 +1,105 @@
+// Copyright (C) 2026 Amalgam Solucoes em TI Ltda
+//
+// SPDX-License-Identifier: LGPL-2.1-only
+
+package totalcross.ui.style.model;
+
+import java.util.Arrays;
+import java.util.Objects;
+
+/**
+ * Represents the elevation of a box as one or more shadow layers.
+ */
+public final class Elevation {
+ /** Constant for no elevation. */
+ public static final Elevation NONE = new Elevation(new Shadow[] {Shadow.NONE});
+
+ public final Shadow[] shadows;
+ public final double outset;
+
+ private Elevation(Shadow[] shadows) {
+ this.shadows = copyShadows(shadows);
+ this.outset = computeOutset(this.shadows);
+ }
+
+ /**
+ * Creates an elevation from explicit shadow layers.
+ */
+ public static Elevation of(Shadow... shadows) {
+ return new Elevation(shadows);
+ }
+
+ /**
+ * Returns a preset elevation for the given elevation level.
+ */
+ public static Elevation of(int elevation) {
+ switch (elevation) {
+ case 0:
+ return NONE;
+ case 1:
+ return of(
+ Shadow.of(0, 1, 2f, 18, 0),
+ Shadow.of(0, 1, 3f, 10, 0)
+ );
+ case 2:
+ return of(
+ Shadow.of(0, 2, 4f, 18, 0),
+ Shadow.of(0, 1, 2f, 12, 0)
+ );
+ case 3:
+ return of(
+ Shadow.of(0, 3, 6f, 18, 0),
+ Shadow.of(0, 1, 3f, 12, 0)
+ );
+ case 4:
+ return of(
+ Shadow.of(0, 2, 4f, 46, 0),
+ Shadow.of(0, 3, 8f, 31, 0)
+ );
+ case 6:
+ return of(
+ Shadow.of(0, 4, 8f, 42, 0),
+ Shadow.of(0, 6, 16f, 24, 0)
+ );
+ case 8:
+ return of(
+ Shadow.of(0, 6, 12f, 40, 0),
+ Shadow.of(0, 8, 20f, 22, 0)
+ );
+ default:
+ return of(
+ Shadow.of(0, elevation, elevation * 2f, 48, 0),
+ Shadow.of(0, Math.max(1, elevation / 2), Math.max(1f, elevation * 1.25f), 24, 0)
+ );
+ }
+ }
+
+ private static Shadow[] copyShadows(Shadow[] shadows) {
+ Objects.requireNonNull(shadows, "Elevation.shadows cannot be null");
+ if (shadows.length == 0) {
+ return new Shadow[] {Shadow.NONE};
+ }
+
+ Shadow[] copy = Arrays.copyOf(shadows, shadows.length);
+ for (int i = 0; i < copy.length; i++) {
+ copy[i] = Objects.requireNonNull(copy[i], "Elevation.shadows[" + i + "] cannot be null");
+ }
+ return copy;
+ }
+
+ private static double computeOutset(Shadow[] shadows) {
+ double expand = 0;
+ for (int i = 0; i < shadows.length; i++) {
+ Shadow shadow = shadows[i];
+ if (shadow == null || shadow == Shadow.NONE || shadow.alpha <= 0) {
+ continue;
+ }
+ expand = Math.max(
+ expand,
+ Math.max(Math.abs(shadow.dx), Math.abs(shadow.dy))
+ + Math.max(0d, shadow.spread)
+ + Math.max(0d, shadow.blurRadius));
+ }
+ return expand;
+ }
+}
diff --git a/TotalCrossSDK/src/main/java/totalcross/ui/style/model/Shadow.java b/TotalCrossSDK/src/main/java/totalcross/ui/style/model/Shadow.java
new file mode 100644
index 000000000..94d66e66d
--- /dev/null
+++ b/TotalCrossSDK/src/main/java/totalcross/ui/style/model/Shadow.java
@@ -0,0 +1,48 @@
+// Copyright (C) 2026 Amalgam Solucoes em TI Ltda
+//
+// SPDX-License-Identifier: LGPL-2.1-only
+
+package totalcross.ui.style.model;
+
+import totalcross.ui.gfx.Color;
+
+/**
+ * Describes a single box shadow layer.
+ */
+public final class Shadow {
+ /** Constant for an empty shadow. */
+ public static final Shadow NONE = new Shadow(0, 0, 0, 0, 0, Color.BLACK);
+
+ public final double dx;
+ public final double dy;
+ public final double blurRadius;
+ public final int alpha;
+ public final double spread;
+ public final int color;
+
+ private Shadow(double dx, double dy, double blurRadius, int alpha, double spread, int color) {
+ this.dx = dx;
+ this.dy = dy;
+ this.blurRadius = blurRadius;
+ this.alpha = alpha;
+ this.spread = spread;
+ this.color = color;
+ }
+
+ /**
+ * Creates a shadow using black as the color.
+ */
+ public static Shadow of(double dx, double dy, double blurRadius, int alpha, double spread) {
+ return of(dx, dy, blurRadius, alpha, spread, Color.BLACK);
+ }
+
+ /**
+ * Creates a shadow.
+ */
+ public static Shadow of(double dx, double dy, double blurRadius, int alpha, double spread, int color) {
+ if (dx == 0 && dy == 0 && blurRadius == 0 && alpha == 0 && spread == 0 && color == Color.BLACK) {
+ return NONE;
+ }
+ return new Shadow(dx, dy, blurRadius, alpha, spread, color);
+ }
+}
diff --git a/TotalCrossSDK/src/main/java/totalcross/ui/style/render/BoxRenderer.java b/TotalCrossSDK/src/main/java/totalcross/ui/style/render/BoxRenderer.java
new file mode 100644
index 000000000..430ba05f9
--- /dev/null
+++ b/TotalCrossSDK/src/main/java/totalcross/ui/style/render/BoxRenderer.java
@@ -0,0 +1,312 @@
+// Copyright (C) 2026 Amalgam Solucoes em TI Ltda
+//
+// SPDX-License-Identifier: LGPL-2.1-only
+
+package totalcross.ui.style.render;
+
+import totalcross.ui.Control;
+import totalcross.ui.Insets;
+import totalcross.ui.gfx.Color;
+import totalcross.ui.gfx.Graphics;
+import totalcross.ui.gfx.RRect;
+import totalcross.ui.gfx.Span;
+import totalcross.ui.style.geom.BoxGeometry;
+import totalcross.ui.style.model.BorderSide;
+import totalcross.ui.style.model.BoxBorder;
+import totalcross.ui.style.model.BoxPaint;
+import totalcross.ui.style.model.BoxShape;
+import totalcross.ui.style.model.BoxStyle;
+import totalcross.ui.style.model.CornerRadii;
+import totalcross.ui.style.model.Elevation;
+import totalcross.ui.style.model.Shadow;
+import totalcross.util.UnitsConverter;
+
+/**
+ * Renders the visual representation of a {@link BoxStyle}.
+ */
+public final class BoxRenderer implements ControlRenderer {
+ private static final double SHADOW_ALPHA_GAIN = 1.8d;
+
+ public final BoxStyle style;
+ public final BoxStyle activeStyle;
+ public final BoxBorder spec;
+ public final CornerRadii radii;
+ public final int backgroundColor;
+ public final int pressedColor;
+ public final Elevation elevation;
+
+ /**
+ * Creates a painter for the given style.
+ */
+ public BoxRenderer(BoxStyle style) {
+ this(style, null);
+ }
+
+ /**
+ * Creates a painter for the given normal and pressed styles.
+ */
+ public BoxRenderer(BoxStyle style, BoxStyle activeStyle) {
+ this.style = style;
+ this.activeStyle = activeStyle;
+ BoxPaint paint = style == null ? null : style.paint;
+ BoxShape shape = style == null ? null : style.shape;
+ this.spec = paint == null ? null : paint.border;
+ this.radii = shape == null || shape.radii == null ? CornerRadii.ZERO : shape.radii;
+ this.backgroundColor = paint == null ? 0 : paint.backgroundColor;
+ this.pressedColor = paint == null ? Color.WHITE : paint.pressedColor;
+ this.elevation = maxOutset(style == null ? Elevation.NONE : style.elevation,
+ activeStyle == null ? Elevation.NONE : activeStyle.elevation);
+ }
+
+ /**
+ * Paints the box for the given bounds.
+ */
+ @Override
+ public void draw(Graphics g, int x, int y, int w, int h, boolean pressed) {
+ if (pressed && activeStyle != null) {
+ drawStyle(g, x, y, w, h, activeStyle);
+ return;
+ }
+
+ BoxGeometry geom = BoxGeometry.compute(x, y, w, h, style);
+ drawElevation(g, geom, style == null ? Elevation.NONE : style.elevation);
+ int bg = pressed ? pressedColor : backgroundColor;
+ g.drawRRect(geom.borderRRect(), 0xFF000000 | bg, true);
+ drawBorderStrip(g, geom, spec);
+ }
+
+ /**
+ * Returns how much the renderer extends beyond the control bounds.
+ */
+ @Override
+ public double getOutset() {
+ return elevation == null ? 0d : elevation.outset;
+ }
+
+ /**
+ * Returns the effective renderer insets, including layout padding and border widths.
+ */
+ @Override
+ public Insets getInsets() {
+ Insets insets = new Insets();
+ if (style != null && style.layout != null) {
+ insets.left += style.layout.padding.left;
+ insets.right += style.layout.padding.right;
+ insets.top += style.layout.padding.top;
+ insets.bottom += style.layout.padding.bottom;
+ }
+ if (spec != null) {
+ Insets borderInsets = spec.getInsets();
+ insets.left += borderInsets.left;
+ insets.right += borderInsets.right;
+ insets.top += borderInsets.top;
+ insets.bottom += borderInsets.bottom;
+ }
+ return insets;
+ }
+
+ /**
+ * Returns whether child painting should be clipped.
+ */
+ @Override
+ public boolean shouldClipChildren() {
+ return style != null && style.clip != null && style.clip.shouldClip();
+ }
+
+ /**
+ * Returns the clip used for children for the given control size.
+ */
+ @Override
+ public RRect getChildrenClip(int width, int height) {
+ if (!shouldClipChildren()) {
+ return null;
+ }
+ BoxGeometry geom = BoxGeometry.compute(0, 0, width, height, style);
+ return geom.paddingRRect();
+ }
+
+ /**
+ * Paints only the border strip for an already computed geometry.
+ */
+ public static void drawBorderStrip(Graphics g, BoxGeometry geom, BoxBorder spec) {
+ if (spec == null || !spec.isVisible()) {
+ return;
+ }
+
+ int outerTop = geom.borderBox.y;
+ int outerBottom = geom.borderBox.y + geom.borderBox.height - 1;
+ int innerTop = geom.paddingBox.y;
+ int innerBottom = geom.paddingBox.y + geom.paddingBox.height - 1;
+
+ for (int yy = outerTop; yy <= outerBottom; yy++) {
+ Span outer = geom.borderRRect().horizontalSpan(yy);
+ if (!outer.isValid()) {
+ continue;
+ }
+
+ if (geom.paddingBox.width <= 0 || geom.paddingBox.height <= 0 || yy < innerTop || yy > innerBottom) {
+ BorderSide side = yy < innerTop ? spec.top : spec.bottom;
+ drawStyledHorizontal(g, side, outer.start, yy, outer.end);
+ continue;
+ }
+
+ Span inner = geom.paddingRRect().horizontalSpan(yy);
+ if (!inner.isValid()) {
+ drawStyledHorizontal(g, spec.top, outer.start, yy, outer.end);
+ continue;
+ }
+
+ if (outer.start < inner.start) {
+ drawStyledHorizontal(g, spec.left, outer.start, yy, inner.start - 1);
+ }
+ if (inner.end < outer.end) {
+ drawStyledHorizontal(g, spec.right, inner.end + 1, yy, outer.end);
+ }
+ }
+ }
+
+ private static void drawElevation(Graphics g, BoxGeometry geom, Elevation elevation) {
+ if (elevation == null || elevation.shadows == null || elevation.shadows.length == 0) {
+ return;
+ }
+ for (int i = 0; i < elevation.shadows.length; i++) {
+ drawShadow(g, geom, elevation.shadows[i]);
+ }
+ }
+
+ private static void drawStyle(Graphics g, int x, int y, int w, int h, BoxStyle style) {
+ BoxGeometry geom = BoxGeometry.compute(x, y, w, h, style);
+ drawElevation(g, geom, style == null ? Elevation.NONE : style.elevation);
+ BoxPaint paint = style == null ? null : style.paint;
+ BoxBorder border = paint == null ? null : paint.border;
+ int background = paint == null ? 0 : paint.backgroundColor;
+ g.drawRRect(geom.borderRRect(), 0xFF000000 | background, true);
+ drawBorderStrip(g, geom, border);
+ }
+
+ private static Elevation maxOutset(Elevation first, Elevation second) {
+ if (first == null) {
+ return second == null ? Elevation.NONE : second;
+ }
+ if (second == null || first.outset >= second.outset) {
+ return first;
+ }
+ return second;
+ }
+
+ private static void drawShadow(Graphics g, BoxGeometry geom, Shadow shadow) {
+ if (shadow == null || shadow.alpha <= 0) {
+ return;
+ }
+
+ int dx = toPixels(shadow.dx);
+ int dy = toPixels(shadow.dy);
+ int spread = Math.max(0, toPixels(shadow.spread));
+ int blurPixels = Math.max(0, toPixels(shadow.blurRadius));
+ int layers = blurPixels <= 0 ? 0 : Math.max(1, Math.min(4, (blurPixels + 2) / 3));
+ int layerStep = layers <= 0 ? 0 : Math.max(1, (int) Math.ceil((double) blurPixels / layers));
+ int totalWeight = layers <= 0 ? 1 : (layers + 1) * (layers + 2) / 2;
+ int savedAlpha = g.alpha;
+ int savedForeColor = g.foreColor;
+
+ try {
+ for (int layer = layers; layer >= 0; layer--) {
+ int blurOffset = layer * layerStep;
+ int inset = -(spread + blurOffset);
+ int shadowX = geom.borderBox.x + dx + inset;
+ int shadowY = geom.borderBox.y + dy + inset;
+ int shadowW = geom.borderBox.width - inset * 2;
+ int shadowH = geom.borderBox.height - inset * 2;
+
+ if (shadowW <= 0 || shadowH <= 0) {
+ continue;
+ }
+
+ int weight = layers - layer + 1;
+ int layerAlpha = Math.max(1, Math.min(255,
+ (int) Math.round(((shadow.alpha * weight) / (double) totalWeight) * SHADOW_ALPHA_GAIN)));
+ CornerRadii shadowRadii = geom.borderRadii
+ .offset(spread + blurOffset)
+ .clamp(shadowW, shadowH);
+
+ g.alpha = (layerAlpha & 0xFF) << 24;
+ g.foreColor = shadow.color;
+ g.drawRRect(
+ new RRect(
+ shadowX,
+ shadowY,
+ shadowW,
+ shadowH,
+ new double[] {
+ shadowRadii.topLeftX, shadowRadii.topLeftY,
+ shadowRadii.topRightX, shadowRadii.topRightY,
+ shadowRadii.bottomRightX, shadowRadii.bottomRightY,
+ shadowRadii.bottomLeftX, shadowRadii.bottomLeftY
+ }
+ ),
+ shadow.color,
+ true
+ );
+ }
+ } finally {
+ g.alpha = savedAlpha;
+ g.foreColor = savedForeColor;
+ }
+ }
+
+ private static int toPixels(double value) {
+ return (int) Math.round(UnitsConverter.toPixels(Control.DP + value));
+ }
+
+ private static void drawStyledHorizontal(Graphics g, BorderSide side, int x1, int y, int x2) {
+ if (side == null || !side.isVisible() || x2 < x1) {
+ return;
+ }
+ g.foreColor = side.color;
+ if (side.style == BorderSide.Style.DASHED) {
+ drawDashedLine(g, x1, y, x2, y);
+ } else if (side.style == BorderSide.Style.DOTTED) {
+ drawDottedLine(g, x1, y, x2, y);
+ } else {
+ g.drawLine(x1, y, x2, y);
+ }
+ }
+
+ private static void drawDashedLine(Graphics g, int x1, int y1, int x2, int y2) {
+ int dash = 4;
+ int gap = 3;
+ int dx = x2 - x1;
+ int dy = y2 - y1;
+ int len = Math.max(Math.abs(dx), Math.abs(dy));
+ if (len == 0) {
+ g.drawLine(x1, y1, x2, y2);
+ return;
+ }
+
+ for (int i = 0; i < len; i += dash + gap) {
+ int sx = x1 + (dx * i) / len;
+ int sy = y1 + (dy * i) / len;
+ int ex = x1 + (dx * Math.min(i + dash, len)) / len;
+ int ey = y1 + (dy * Math.min(i + dash, len)) / len;
+ g.drawLine(sx, sy, ex, ey);
+ }
+ }
+
+ private static void drawDottedLine(Graphics g, int x1, int y1, int x2, int y2) {
+ int step = 3;
+ int dx = x2 - x1;
+ int dy = y2 - y1;
+ int len = Math.max(Math.abs(dx), Math.abs(dy));
+ if (len == 0) {
+ g.setPixel(x1, y1);
+ return;
+ }
+
+ for (int i = 0; i < len; i += step) {
+ int px = x1 + (dx * i) / len;
+ int py = y1 + (dy * i) / len;
+ g.setPixel(px, py);
+ }
+ }
+
+}
diff --git a/TotalCrossSDK/src/main/java/totalcross/ui/style/render/ControlRenderer.java b/TotalCrossSDK/src/main/java/totalcross/ui/style/render/ControlRenderer.java
new file mode 100644
index 000000000..d213e48c4
--- /dev/null
+++ b/TotalCrossSDK/src/main/java/totalcross/ui/style/render/ControlRenderer.java
@@ -0,0 +1,39 @@
+// Copyright (C) 2026 Amalgam Solucoes em TI Ltda
+//
+// SPDX-License-Identifier: LGPL-2.1-only
+
+package totalcross.ui.style.render;
+
+import totalcross.ui.Insets;
+import totalcross.ui.gfx.Graphics;
+import totalcross.ui.gfx.RRect;
+
+/**
+ * Describes the visual rendering contract used by a control.
+ */
+public interface ControlRenderer {
+ /**
+ * Paints the control for the given bounds.
+ */
+ void draw(Graphics g, int x, int y, int w, int h, boolean pressed);
+
+ /**
+ * Returns how much the renderer extends beyond the control bounds.
+ */
+ double getOutset();
+
+ /**
+ * Returns the effective insets contributed by the renderer.
+ */
+ Insets getInsets();
+
+ /**
+ * Returns whether child painting should be clipped.
+ */
+ boolean shouldClipChildren();
+
+ /**
+ * Returns the clip used for children at the given control size, or {@code null} when child clipping is disabled.
+ */
+ RRect getChildrenClip(int width, int height);
+}
diff --git a/TotalCrossSDK/src/main/java/totalcross/util/UnitsConverter.java b/TotalCrossSDK/src/main/java/totalcross/util/UnitsConverter.java
index b6f0f6713..753fdda7c 100644
--- a/TotalCrossSDK/src/main/java/totalcross/util/UnitsConverter.java
+++ b/TotalCrossSDK/src/main/java/totalcross/util/UnitsConverter.java
@@ -1,14 +1,41 @@
+// Copyright (C) 2014-2020 TotalCross Global Mobile Platform Ltda.
+// Copyright (C) 2020-2026 Amalgam Solucoes em TI Ltda
+//
+// SPDX-License-Identifier: LGPL-2.1-only
+
package totalcross.util;
import totalcross.sys.Settings;
import totalcross.ui.Control;
+/**
+ * Utility methods for converting unit-encoded values into screen pixels.
+ */
public abstract class UnitsConverter {
+ /**
+ * Converts an integer unit-encoded value to pixels.
+ *
+ * @param value the raw value, optionally encoded with a unit marker such as {@code Control.DP}
+ * @return the converted pixel value, or the original value when no conversion is needed
+ */
public static int toPixels(int value) {
if ((Control.DP - Control.RANGE) <= value && value <= (Control.DP + Control.RANGE)) {
return (int) (Settings.screenDensity * (value - Control.DP));
}
return value;
}
+
+ /**
+ * Converts a floating-point unit-encoded value to pixels.
+ *
+ * @param value the raw value, optionally encoded with a unit marker such as {@code Control.DP}
+ * @return the converted pixel value, or the original value when no conversion is needed
+ */
+ public static double toPixels(double value) {
+ if ((Control.DP - Control.RANGE) <= value && value <= (Control.DP + Control.RANGE)) {
+ return Settings.screenDensity * (value - Control.DP);
+ }
+ return value;
+ }
}
diff --git a/TotalCrossVM/CMakeLists.txt b/TotalCrossVM/CMakeLists.txt
index 1f5ac58ee..45d4fbd6f 100644
--- a/TotalCrossVM/CMakeLists.txt
+++ b/TotalCrossVM/CMakeLists.txt
@@ -466,6 +466,7 @@ set(SOURCES
${TC_SRCDIR}/nm/sys/Convert.c
${TC_SRCDIR}/nm/ui/gfx_Graphics.c
+ ${TC_SRCDIR}/nm/ui/gfx_RRect.c
${TC_SRCDIR}/nm/ui/event_Event.c
${TC_SRCDIR}/nm/ui/Control.c
${TC_SRCDIR}/nm/ui/font_Font.c
diff --git a/TotalCrossVM/src/init/nativeProcAddressesTC.c b/TotalCrossVM/src/init/nativeProcAddressesTC.c
index 12e84699a..1ddb88b01 100644
--- a/TotalCrossVM/src/init/nativeProcAddressesTC.c
+++ b/TotalCrossVM/src/init/nativeProcAddressesTC.c
@@ -132,7 +132,7 @@ void fillNativeProcAddressesTC()
htPutPtr(&htNativeProcAddresses, hashCode("tugG_getPixel_ii"), &tugG_getPixel_ii);
htPutPtr(&htNativeProcAddresses, hashCode("tugG_setPixel_ii"), &tugG_setPixel_ii);
htPutPtr(&htNativeProcAddresses, hashCode("tugG_drawLine_iiii"), &tugG_drawLine_iiii);
- htPutPtr(&htNativeProcAddresses, hashCode("tugG_drawLine_iiiii"), &tugG_drawLine_iiiii);
+ htPutPtr(&htNativeProcAddresses, hashCode("tugG_drawLine_iiiip"), &tugG_drawLine_iiiip);
htPutPtr(&htNativeProcAddresses, hashCode("tugG_drawDots_iiii"), &tugG_drawDots_iiii);
htPutPtr(&htNativeProcAddresses, hashCode("tugG_drawRect_iiii"), &tugG_drawRect_iiii);
htPutPtr(&htNativeProcAddresses, hashCode("tugG_fillRect_iiii"), &tugG_fillRect_iiii);
@@ -142,6 +142,7 @@ void fillNativeProcAddressesTC()
htPutPtr(&htNativeProcAddresses, hashCode("tugG_drawText_siii"), &tugG_drawText_siii);
htPutPtr(&htNativeProcAddresses, hashCode("tugG_drawRoundRect_iiiii"), &tugG_drawRoundRect_iiiii);
htPutPtr(&htNativeProcAddresses, hashCode("tugG_fillRoundRect_iiiii"), &tugG_fillRoundRect_iiiii);
+ htPutPtr(&htNativeProcAddresses, hashCode("tugG_drawRRect_rib"), &tugG_drawRRect_rib);
htPutPtr(&htNativeProcAddresses, hashCode("tugG_copyRect_giiiiii"), &tugG_copyRect_giiiiii);
htPutPtr(&htNativeProcAddresses, hashCode("tugG_drawRoundGradient_iiiiiiiii"), &tugG_drawRoundGradient_iiiiiiiii);
htPutPtr(&htNativeProcAddresses, hashCode("tugG_drawImage_iiib"), &tugG_drawImage_iiib);
@@ -156,6 +157,12 @@ void fillNativeProcAddressesTC()
htPutPtr(&htNativeProcAddresses, hashCode("tugG_fillPolygonGradient_IIi"), &tugG_fillPolygonGradient_IIi);
htPutPtr(&htNativeProcAddresses, hashCode("tugG_getRGB_Iiiiii"), &tugG_getRGB_Iiiiii);
htPutPtr(&htNativeProcAddresses, hashCode("tugG_setRGB_Iiiiii"), &tugG_setRGB_Iiiiii);
+ htPutPtr(&htNativeProcAddresses, hashCode("tugRR_containsCorner_ddddddi"), &tugRR_containsCorner_ddddddi);
+ htPutPtr(&htNativeProcAddresses, hashCode("tugRR_leftBoundForY_dddddddd"), &tugRR_leftBoundForY_dddddddd);
+ htPutPtr(&htNativeProcAddresses, hashCode("tugRR_rightBoundForY_dddddddd"), &tugRR_rightBoundForY_dddddddd);
+ htPutPtr(&htNativeProcAddresses, hashCode("tugRR_ellipseLeftBound_ddddd"), &tugRR_ellipseLeftBound_ddddd);
+ htPutPtr(&htNativeProcAddresses, hashCode("tugRR_ellipseRightBound_ddddd"), &tugRR_ellipseRightBound_ddddd);
+ htPutPtr(&htNativeProcAddresses, hashCode("tugRR_ellipseDx_ddd"), &tugRR_ellipseDx_ddd);
htPutPtr(&htNativeProcAddresses, hashCode("tugG_fadeScreen_i"), &tugG_fadeScreen_i);
htPutPtr(&htNativeProcAddresses, hashCode("tufF_fontCreate"), &tufF_fontCreate);
htPutPtr(&htNativeProcAddresses, hashCode("tufFM_fontMetricsCreate"), &tufFM_fontMetricsCreate);
diff --git a/TotalCrossVM/src/nm/NativeMethods.h b/TotalCrossVM/src/nm/NativeMethods.h
index d296aa0ea..83592da31 100644
--- a/TotalCrossVM/src/nm/NativeMethods.h
+++ b/TotalCrossVM/src/nm/NativeMethods.h
@@ -145,7 +145,7 @@ TC_API void tugG_fillCircle_iii(NMParams p);
TC_API void tugG_getPixel_ii(NMParams p);
TC_API void tugG_setPixel_ii(NMParams p);
TC_API void tugG_drawLine_iiii(NMParams p);
-TC_API void tugG_drawLine_iiiii(NMParams p);
+TC_API void tugG_drawLine_iiiip(NMParams p);
TC_API void tugG_drawDots_iiii(NMParams p);
TC_API void tugG_drawRect_iiii(NMParams p);
TC_API void tugG_fillRect_iiii(NMParams p);
@@ -157,6 +157,13 @@ TC_API void tugG_drawText_Ciiii(NMParams p);
TC_API void tugG_drawText_siii(NMParams p);
TC_API void tugG_drawRoundRect_iiiii(NMParams p);
TC_API void tugG_fillRoundRect_iiiii(NMParams p);
+TC_API void tugG_drawRRect_rib(NMParams p);
+TC_API void tugRR_containsCorner_ddddddi(NMParams p);
+TC_API void tugRR_leftBoundForY_dddddddd(NMParams p);
+TC_API void tugRR_rightBoundForY_dddddddd(NMParams p);
+TC_API void tugRR_ellipseLeftBound_ddddd(NMParams p);
+TC_API void tugRR_ellipseRightBound_ddddd(NMParams p);
+TC_API void tugRR_ellipseDx_ddd(NMParams p);
TC_API void tugG_copyRect_giiiiii(NMParams p);
TC_API void tugG_drawRoundGradient_iiiiiiiii(NMParams p);
TC_API void tugG_drawImage_iiib(NMParams p);
diff --git a/TotalCrossVM/src/nm/NativeMethods.txt b/TotalCrossVM/src/nm/NativeMethods.txt
index 53611c745..121e314e7 100644
--- a/TotalCrossVM/src/nm/NativeMethods.txt
+++ b/TotalCrossVM/src/nm/NativeMethods.txt
@@ -123,7 +123,7 @@ totalcross/ui/gfx/Graphics|native public void fillCircle(int xc, int yc, int r);
totalcross/ui/gfx/Graphics|native public int getPixel(int x, int y);
totalcross/ui/gfx/Graphics|native public void setPixel(int x, int y);
totalcross/ui/gfx/Graphics|native public void drawLine(int ax, int ay, int bx, int by);
-totalcross/ui/gfx/Graphics|native public void drawLine(int ax, int ay, int bx, int by, int c);
+totalcross/ui/gfx/Graphics|native public void drawLine(int ax, int ay, int bx, int by, totalcross.ui.gfx.Paint paint);
totalcross/ui/gfx/Graphics|native public void drawDots(int ax, int ay, int bx, int by);
totalcross/ui/gfx/Graphics|native public void drawRect(int x, int y, int w, int h);
totalcross/ui/gfx/Graphics|native public void fillRect(int x, int y, int w, int h);
@@ -133,6 +133,13 @@ totalcross/ui/gfx/Graphics|native public void drawPolyline(int []xPoints, int []
totalcross/ui/gfx/Graphics|native public void drawText(String text, int x, int y, int justifyWidth);
totalcross/ui/gfx/Graphics|native public void drawRoundRect(int x, int y, int width, int height, int r);
totalcross/ui/gfx/Graphics|native public void fillRoundRect(int x, int y, int width, int height, int r);
+totalcross/ui/gfx/Graphics|native public void drawRRect(totalcross.ui.gfx.RRect rrect, int color, boolean filled);
+totalcross/ui/gfx/RRect|native private static boolean containsCorner(double px, double py, double x, double y, double rx, double ry, int corner);
+totalcross/ui/gfx/RRect|native private static double leftBoundForY(double py, double left, double top, double bottom, double topRx, double topRy, double bottomRx, double bottomRy);
+totalcross/ui/gfx/RRect|native private static double rightBoundForY(double py, double right, double top, double bottom, double topRx, double topRy, double bottomRx, double bottomRy);
+totalcross/ui/gfx/RRect|native private static double ellipseLeftBound(double cx, double cy, double rx, double ry, double py);
+totalcross/ui/gfx/RRect|native private static double ellipseRightBound(double cx, double cy, double rx, double ry, double py);
+totalcross/ui/gfx/RRect|native private static double ellipseDx(double rx, double ry, double dy);
totalcross/ui/gfx/Graphics|native public void copyRect(totalcross.ui.gfx.GfxSurface surface, int x, int y, int width, int height, int dstX, int dstY);
totalcross/ui/gfx/Graphics|native public void drawRoundGradient(int startX, int startY, int endX, int endY, int topLeftRadius, int topRightRadius, int bottomLeftRadius, int bottomRightRadius,int startColor, int endColor, boolean vertical);
totalcross/ui/gfx/Graphics|native public void drawImage(totalcross.ui.image.Image image, int x, int y, boolean doClip);
diff --git a/TotalCrossVM/src/nm/NativeMethodsPrototypes.txt b/TotalCrossVM/src/nm/NativeMethodsPrototypes.txt
index 80dc82926..8e9d230b4 100644
--- a/TotalCrossVM/src/nm/NativeMethodsPrototypes.txt
+++ b/TotalCrossVM/src/nm/NativeMethodsPrototypes.txt
@@ -125,7 +125,7 @@ TC_API void tugG_fillCircle_iii(NMParams p);
TC_API void tugG_getPixel_ii(NMParams p);
TC_API void tugG_setPixel_ii(NMParams p);
TC_API void tugG_drawLine_iiii(NMParams p);
-TC_API void tugG_drawLine_iiiii(NMParams p);
+TC_API void tugG_drawLine_iiiip(NMParams p);
TC_API void tugG_drawDots_iiii(NMParams p);
TC_API void tugG_drawRect_iiii(NMParams p);
TC_API void tugG_fillRect_iiii(NMParams p);
@@ -135,6 +135,13 @@ TC_API void tugG_drawPolyline_IIi(NMParams p);
TC_API void tugG_drawText_siii(NMParams p);
TC_API void tugG_drawRoundRect_iiiii(NMParams p);
TC_API void tugG_fillRoundRect_iiiii(NMParams p);
+TC_API void tugG_drawRRect_rib(NMParams p);
+TC_API void tugRR_containsCorner_ddddddi(NMParams p);
+TC_API void tugRR_leftBoundForY_dddddddd(NMParams p);
+TC_API void tugRR_rightBoundForY_dddddddd(NMParams p);
+TC_API void tugRR_ellipseLeftBound_ddddd(NMParams p);
+TC_API void tugRR_ellipseRightBound_ddddd(NMParams p);
+TC_API void tugRR_ellipseDx_ddd(NMParams p);
TC_API void tugG_copyRect_giiiiii(NMParams p);
TC_API void tugG_drawRoundGradient_iiiiiiiii(NMParams p);
TC_API void tugG_drawImage_iiib(NMParams p);
@@ -1050,7 +1057,7 @@ TC_API void tugG_drawLine_iiii(NMParams p) // totalcross/ui/gfx/Graphics native
{
}
//////////////////////////////////////////////////////////////////////////
-TC_API void tugG_drawLine_iiiii(NMParams p) // totalcross/ui/gfx/Graphics native public void drawLine(int ax, int ay, int bx, int by, int c);
+TC_API void tugG_drawLine_iiiip(NMParams p) // totalcross/ui/gfx/Graphics native public void drawLine(int ax, int ay, int bx, int by, totalcross.ui.gfx.Paint paint);
{
}
//////////////////////////////////////////////////////////////////////////
@@ -1090,6 +1097,34 @@ TC_API void tugG_fillRoundRect_iiiii(NMParams p) // totalcross/ui/gfx/Graphics n
{
}
//////////////////////////////////////////////////////////////////////////
+TC_API void tugG_drawRRect_rib(NMParams p) // totalcross/ui/gfx/Graphics native public void drawRRect(totalcross.ui.gfx.RRect rrect, int color, boolean filled);
+{
+}
+//////////////////////////////////////////////////////////////////////////
+TC_API void tugRR_containsCorner_ddddddi(NMParams p) // totalcross/ui/gfx/RRect native private static boolean containsCorner(double px, double py, double x, double y, double rx, double ry, int corner);
+{
+}
+//////////////////////////////////////////////////////////////////////////
+TC_API void tugRR_leftBoundForY_dddddddd(NMParams p) // totalcross/ui/gfx/RRect native private static double leftBoundForY(double py, double left, double top, double bottom, double topRx, double topRy, double bottomRx, double bottomRy);
+{
+}
+//////////////////////////////////////////////////////////////////////////
+TC_API void tugRR_rightBoundForY_dddddddd(NMParams p) // totalcross/ui/gfx/RRect native private static double rightBoundForY(double py, double right, double top, double bottom, double topRx, double topRy, double bottomRx, double bottomRy);
+{
+}
+//////////////////////////////////////////////////////////////////////////
+TC_API void tugRR_ellipseLeftBound_ddddd(NMParams p) // totalcross/ui/gfx/RRect native private static double ellipseLeftBound(double cx, double cy, double rx, double ry, double py);
+{
+}
+//////////////////////////////////////////////////////////////////////////
+TC_API void tugRR_ellipseRightBound_ddddd(NMParams p) // totalcross/ui/gfx/RRect native private static double ellipseRightBound(double cx, double cy, double rx, double ry, double py);
+{
+}
+//////////////////////////////////////////////////////////////////////////
+TC_API void tugRR_ellipseDx_ddd(NMParams p) // totalcross/ui/gfx/RRect native private static double ellipseDx(double rx, double ry, double dy);
+{
+}
+//////////////////////////////////////////////////////////////////////////
TC_API void tugG_copyRect_giiiiii(NMParams p) // totalcross/ui/gfx/Graphics native public void copyRect(totalcross.ui.gfx.GfxSurface surface, int x, int y, int width, int height, int dstX, int dstY);
{
}
diff --git a/TotalCrossVM/src/nm/instancefields.h b/TotalCrossVM/src/nm/instancefields.h
index 4f67f76cf..362ad17bf 100644
--- a/TotalCrossVM/src/nm/instancefields.h
+++ b/TotalCrossVM/src/nm/instancefields.h
@@ -143,11 +143,14 @@
#define Graphics_lastPPD(o) FIELD_DBL(o, OBJ_CLASS(o), 0)
-#define Graphics_surface(o) FIELD_OBJ(o, OBJ_CLASS(o), 0)
-#define Graphics_font(o) FIELD_OBJ(o, OBJ_CLASS(o), 1)
-#define Graphics_xPoints(o) FIELD_OBJ(o, OBJ_CLASS(o), 2)
-#define Graphics_yPoints(o) FIELD_OBJ(o, OBJ_CLASS(o), 3)
-#define Graphics_ints(o) FIELD_OBJ(o, OBJ_CLASS(o), 4)
+#define Graphics_roundClip(o) FIELD_OBJ(o, OBJ_CLASS(o), 0)
+#define Graphics_surface(o) FIELD_OBJ(o, OBJ_CLASS(o), 1)
+#define Graphics_font(o) FIELD_OBJ(o, OBJ_CLASS(o), 2)
+#define Graphics_xPoints(o) FIELD_OBJ(o, OBJ_CLASS(o), 3)
+#define Graphics_yPoints(o) FIELD_OBJ(o, OBJ_CLASS(o), 4)
+#define Graphics_ints(o) FIELD_OBJ(o, OBJ_CLASS(o), 5)
+#define Graphics_cyPoints(o) FIELD_OBJ(o, OBJ_CLASS(o), 6)
+#define Graphics_cxPoints(o) FIELD_OBJ(o, OBJ_CLASS(o), 7)
// totalcross.ui.image.Image
#define Image_width(o) FIELD_I32(o, 1)
diff --git a/TotalCrossVM/src/nm/ui/GraphicsPrimitives_c.h b/TotalCrossVM/src/nm/ui/GraphicsPrimitives_c.h
index a658b470c..ac0b75828 100644
--- a/TotalCrossVM/src/nm/ui/GraphicsPrimitives_c.h
+++ b/TotalCrossVM/src/nm/ui/GraphicsPrimitives_c.h
@@ -65,6 +65,68 @@ static bool translateAndClip(TCObject g, int32 *pX, int32 *pY, int32 *pWidth, in
#define Get_Clip(g) Graphics_clipX1(g), Graphics_clipY1(g), Graphics_clipX2(g), Graphics_clipY2(g)
+static bool gfxClipContains(TCObject g, int32 x, int32 y)
+{
+ TCObject roundClip = Graphics_roundClip(g);
+ int32 start, end;
+
+ if (x < Graphics_clipX1(g) || x >= Graphics_clipX2(g) || y < Graphics_clipY1(g) || y >= Graphics_clipY2(g))
+ return false;
+
+ if (roundClip == null)
+ return true;
+
+ if (!gfxComputeRRectSpan(
+ Rect_x(roundClip),
+ Rect_y(roundClip),
+ Rect_width(roundClip),
+ Rect_height(roundClip),
+ RRect_radii(roundClip),
+ y,
+ &start,
+ &end))
+ return false;
+
+ return start <= x && x <= end;
+}
+
+static bool gfxClipHorizontalSpan(TCObject g, int32 y, int32 *start, int32 *end)
+{
+ TCObject roundClip = Graphics_roundClip(g);
+ int32 clipStart = Graphics_clipX1(g);
+ int32 clipEnd = Graphics_clipX2(g) - 1;
+ int32 roundStart, roundEnd;
+
+ if (y < Graphics_clipY1(g) || y >= Graphics_clipY2(g))
+ return false;
+
+ if (roundClip != null)
+ {
+ if (!gfxComputeRRectSpan(
+ Rect_x(roundClip),
+ Rect_y(roundClip),
+ Rect_width(roundClip),
+ Rect_height(roundClip),
+ RRect_radii(roundClip),
+ y,
+ &roundStart,
+ &roundEnd))
+ return false;
+
+ if (clipStart < roundStart)
+ clipStart = roundStart;
+ if (clipEnd > roundEnd)
+ clipEnd = roundEnd;
+ }
+
+ if (clipEnd < clipStart)
+ return false;
+
+ *start = clipStart;
+ *end = clipEnd;
+ return true;
+}
+
// >>>>>>>>>
// DO NOT LOCK THE SCREEN ON THESE METHODS. THE CALLER MUST DO THAT.
static Pixel* getSurfacePixels(TCObject surf)
@@ -228,6 +290,7 @@ static void drawSurface(Context currentContext, TCObject dstSurf, TCObject srcSu
Pixel * srcPixels;
Pixel * dstPixels;
int32 srcPitch, srcWidth, srcHeight, alphaMask = 0;
+ TCObject roundClip = Graphics_roundClip(dstSurf);
bool isSrcScreen = !Surface_isImage(srcSurf);
bool unlockSrc = false;
if (Surface_isImage(srcSurf))
@@ -357,9 +420,39 @@ static void drawSurface(Context currentContext, TCObject dstSurf, TCObject srcSu
#endif
for (i=0; i < (uint32)height; i++) // in opengl, only case of image drawing on image
{
- PixelConv *ps = (PixelConv*)srcPixels;
- PixelConv *pt = (PixelConv*)dstPixels;
- uint32 count = width;
+ int32 currentY = dstY + (int32)i;
+ int32 rowStart = dstX;
+ int32 rowEnd = dstX + width - 1;
+ int32 rowWidth;
+ PixelConv *ps;
+ PixelConv *pt;
+ uint32 count;
+
+ if (roundClip != null)
+ {
+ int32 spanStart, spanEnd;
+ if (!gfxClipHorizontalSpan(dstSurf, currentY, &spanStart, &spanEnd))
+ {
+ srcPixels += srcPitch;
+ dstPixels += Graphics_pitch(dstSurf);
+ continue;
+ }
+ if (rowStart < spanStart)
+ rowStart = spanStart;
+ if (rowEnd > spanEnd)
+ rowEnd = spanEnd;
+ if (rowEnd < rowStart)
+ {
+ srcPixels += srcPitch;
+ dstPixels += Graphics_pitch(dstSurf);
+ continue;
+ }
+ }
+
+ rowWidth = rowEnd - rowStart + 1;
+ ps = (PixelConv*)srcPixels + (rowStart - dstX);
+ pt = (PixelConv*)dstPixels + (rowStart - dstX);
+ count = (uint32)rowWidth;
if (isSrcScreen)
for (;count != 0; pt++,ps++, count--)
{
@@ -534,7 +627,7 @@ static void drawSurface(Context currentContext, TCObject dstSurf, TCObject srcSu
}
if (doClip) {
- skia_setClip(Get_Clip(dstSurf));
+ skia_applyClip(dstSurf);
clipSet = true;
}
if (frameCount > 1) {
@@ -574,7 +667,7 @@ static int32 getPixel(TCObject g, int32 x, int32 y)
int32 ret = -1;
x += Graphics_transX(g);
y += Graphics_transY(g);
- if (Graphics_clipX1(g) <= x && x < Graphics_clipX2(g) && Graphics_clipY1(g) <= y && y < Graphics_clipY2(g))
+ if (gfxClipContains(g, x, y))
{
PixelConv p;
#ifdef SKIA_H
@@ -599,7 +692,7 @@ static PixelConv getPixelConv(TCObject g, int32 x, int32 y)
p.pixel = -1;
x += Graphics_transX(g);
y += Graphics_transY(g);
- if (Graphics_clipX1(g) <= x && x < Graphics_clipX2(g) && Graphics_clipY1(g) <= y && y < Graphics_clipY2(g))
+ if (gfxClipContains(g, x, y))
{
#ifdef __gl2_h_
if (Graphics_useOpenGL(g))
@@ -619,7 +712,7 @@ static void setPixel(Context currentContext, TCObject g, int32 x, int32 y, Pixel
{
x += Graphics_transX(g);
y += Graphics_transY(g);
- if (Graphics_clipX1(g) <= x && x < Graphics_clipX2(g) && Graphics_clipY1(g) <= y && y < Graphics_clipY2(g))
+ if (gfxClipContains(g, x, y))
{
#ifdef __gl2_h_
if (Graphics_useOpenGL(g))
@@ -643,7 +736,7 @@ static void setPixel(Context currentContext, TCObject g, int32 x, int32 y, Pixel
{
x += Graphics_transX(g);
y += Graphics_transY(g);
- skia_setClip(Get_Clip(g));
+ skia_applyClip(g);
skia_setPixel(0, x, y, pixel | Graphics_alpha(g));
skia_restoreClip();
@@ -684,22 +777,22 @@ static bool surelyOutsideClip(TCObject g, int32 x1, int32 y1, int32 x2, int32 y2
// Draws a horizontal line from x to x+w-1, translating and clipping
static void drawHLine(Context currentContext, TCObject g, int32 x, int32 y, int32 width, Pixel pixel1, Pixel pixel2)
{
+ int32 clipStart, clipEnd;
+
x += Graphics_transX(g);
y += Graphics_transY(g);
- /*
- | line must lie inside y clip bounds, must not end before clip x1
- | and must not start after clip x2
- */
- if (Graphics_clipY1(g) <= y && y < Graphics_clipY2(g) && Graphics_clipX1(g) <= (x+width) && x < Graphics_clipX2(g)) // NOPT
+ if (width <= 0 || !gfxClipHorizontalSpan(g, y, &clipStart, &clipEnd) || clipStart > (x + width - 1) || x > clipEnd)
+ return;
+
{
Pixel* pTgt;
- if (x < Graphics_clipX1(g)) // line start before clip x1
+ if (x < clipStart)
{
- width -= Graphics_clipX1(g)-x;
- x = Graphics_clipX1(g);
+ width -= clipStart - x;
+ x = clipStart;
}
- if ((x+width) > Graphics_clipX2(g)) // line stops after clip x2
- width = Graphics_clipX2(g)-x;
+ if ((x + width - 1) > clipEnd)
+ width = clipEnd - x + 1;
if (width <= 0)
return;
@@ -736,8 +829,46 @@ static void drawHLine(Context currentContext, TCObject g, int32 x, int32 y, int3
// Draws a vertical line from y to y+h-1 using the given color
static void drawVLine(Context currentContext, TCObject g, int32 x, int32 y, int32 height, Pixel pixel1, Pixel pixel2)
{
+ TCObject roundClip = Graphics_roundClip(g);
x += Graphics_transX(g);
y += Graphics_transY(g);
+
+ if (roundClip != null)
+ {
+ int32 startY = y;
+ int32 endY = y + height;
+ Pixel *pTgt;
+ int32 pitch = Graphics_pitch(g);
+ uint32 i = 0;
+
+ if (x < Graphics_clipX1(g) || x >= Graphics_clipX2(g))
+ return;
+ if (startY < Graphics_clipY1(g))
+ startY = Graphics_clipY1(g);
+ if (endY > Graphics_clipY2(g))
+ endY = Graphics_clipY2(g);
+ if (endY <= startY)
+ return;
+#ifdef __gl2_h_
+ if (Graphics_useOpenGL(g))
+ {
+ int32 yy;
+ for (yy = startY; yy < endY; yy++)
+ if (gfxClipContains(g, x, yy))
+ glDrawPixel(x, yy, pixel1 == pixel2 ? pixel1 : ((i++ & 1) ? pixel1 : pixel2), 255);
+ currentContext->fullDirty = true;
+ return;
+ }
+#endif
+ pTgt = getGraphicsPixels(g) + startY * pitch + x;
+ for (y = startY; y < endY; y++, pTgt += pitch)
+ if (gfxClipContains(g, x, y))
+ *pTgt = pixel1 == pixel2 ? pixel1 : ((i++ & 1) ? pixel1 : pixel2);
+
+ if (!currentContext->fullDirty && !Graphics_isImageSurface(g))
+ markScreenDirty(currentContext, x, startY, 1, endY - startY);
+ return;
+ }
/*
| line must lie inside x clip bounds, must not end before clip y1
| and must not start after clip y2
@@ -842,7 +973,7 @@ static void drawDottedLine(Context currentContext, TCObject g, int32 x1, int32 y
Pixel *row;
int32 on = 1;
int32 clipX1 = Graphics_clipX1(g), clipY1 = Graphics_clipY1(g), clipX2 = Graphics_clipX2(g), clipY2 = Graphics_clipY2(g);
- bool dontClip = true; // the most common will be draw lines that do not cross the clip bounds, so we may speedup a little
+ bool dontClip = Graphics_roundClip(g) == null; // the most common will be draw lines that do not cross the clip bounds, so we may speedup a little
xMin += tX;
yMin += tY;
@@ -869,7 +1000,7 @@ static void drawDottedLine(Context currentContext, TCObject g, int32 x1, int32 y
if (pixel1 == pixel2) // quick optimization
for (; dX >= 0; dX--) // process each point in the line one at a time (just use dX)
{
- if (dontClip || (clipX1 <= currentX && currentX < clipX2 && clipY1 <= currentY && currentY < clipY2))
+ if (dontClip || gfxClipContains(g, currentX, currentY))
{
#ifdef __gl2_h_
if (Graphics_useOpenGL(g))
@@ -892,7 +1023,7 @@ static void drawDottedLine(Context currentContext, TCObject g, int32 x1, int32 y
else
for (; dX >= 0; dX--) // process each point in the line one at a time (just use dX)
{
- if (dontClip || (clipX1 <= currentX && currentX < clipX2 && clipY1 <= currentY && currentY < clipY2))
+ if (dontClip || gfxClipContains(g, currentX, currentY))
{
#ifdef __gl2_h_
if (Graphics_useOpenGL(g))
@@ -922,7 +1053,7 @@ static void drawDottedLine(Context currentContext, TCObject g, int32 x1, int32 y
if (pixel1 == pixel2) // quick optimization
for (; dY >= 0; dY--) // process each point in the line one at a time (just use dY)
{
- if (dontClip || (clipX1 <= currentX && currentX < clipX2 && clipY1 <= currentY && currentY < clipY2))
+ if (dontClip || gfxClipContains(g, currentX, currentY))
{
#ifdef __gl2_h_
if (Graphics_useOpenGL(g))
@@ -945,7 +1076,7 @@ static void drawDottedLine(Context currentContext, TCObject g, int32 x1, int32 y
else
for (; dY >= 0; dY--) // process each point in the line one at a time (just use dY)
{
- if (dontClip || (clipX1 <= currentX && currentX < clipX2 && clipY1 <= currentY && currentY < clipY2))
+ if (dontClip || gfxClipContains(g, currentX, currentY))
{
#ifdef __gl2_h_
if (Graphics_useOpenGL(g))
@@ -983,7 +1114,7 @@ static void drawDottedLine(Context currentContext, TCObject g, int32 x1, int32 y
y1 += Graphics_transY(g);
x2 += Graphics_transX(g);
y2 += Graphics_transY(g);
- skia_setClip(Get_Clip(g));
+ skia_applyClip(g);
skia_drawDottedLine(0, x1, y1, x2, y2, pixel1 | Graphics_alpha(g), pixel2 | Graphics_alpha(g));
skia_restoreClip();
@@ -997,19 +1128,21 @@ static int32 abs32(int32 a)
return a < 0 ? -a : a;
}
-static void drawLine(Context currentContext, TCObject g, int32 x1, int32 y1, int32 x2, int32 y2, Pixel pixel)
+static void drawLine(Context currentContext, TCObject g, int32 x1, int32 y1, int32 x2, int32 y2, GfxPaint paint)
{
+ Pixel pixel = *paint.color;
drawDottedLine(currentContext, g, x1, y1, x2, y2, pixel, pixel);
}
#else
-static void drawLine(Context currentContext, TCObject g, int32 x1, int32 y1, int32 x2, int32 y2, Pixel pixel)
+static void drawLine(Context currentContext, TCObject g, int32 x1, int32 y1, int32 x2, int32 y2, GfxPaint paint)
{
+ Pixel pixel = *paint.color;
x1 += Graphics_transX(g);
y1 += Graphics_transY(g);
x2 += Graphics_transX(g);
y2 += Graphics_transY(g);
- skia_setClip(Get_Clip(g));
- skia_drawLine(0, x1, y1, x2, y2, pixel | Graphics_alpha(g));
+ skia_applyClip(g);
+ skia_drawLine(0, x1, y1, x2, y2, paint);
skia_restoreClip();
markDirty(currentContext, g, min32(x1, x2), min32(y1, y2), abs(x2 - x1), abs(y2 - y1));
@@ -1030,7 +1163,7 @@ static void drawRect(Context currentContext, TCObject g, int32 x, int32 y, int32
{
x += Graphics_transX(g);
y += Graphics_transY(g);
- skia_setClip(Get_Clip(g));
+ skia_applyClip(g);
skia_drawRect(0, x, y, w, h, pixel | Graphics_alpha(g));
skia_restoreClip();
@@ -1044,6 +1177,7 @@ static void drawRect(Context currentContext, TCObject g, int32 x, int32 y, int32
#ifndef SKIA_H
static void fillRect(Context currentContext, TCObject g, int32 x, int32 y, int32 width, int32 height, Pixel pixel)
{
+ TCObject roundClip = Graphics_roundClip(g);
int32 clipX1 = Graphics_clipX1(g);
int32 clipX2 = Graphics_clipX2(g);
int32 clipY1 = Graphics_clipY1(g);
@@ -1069,6 +1203,40 @@ static void fillRect(Context currentContext, TCObject g, int32 x, int32 y, int32
if (height > 0 && width > 0)
{
+ if (roundClip != null)
+ {
+ int32 pitch = Graphics_pitch(g);
+ Pixel *base = getGraphicsPixels(g);
+ int32 yy;
+
+ for (yy = y; yy < y + height; yy++)
+ {
+ int32 spanStart, spanEnd;
+ int32 rowX = x;
+ int32 rowWidth = width;
+ Pixel *to;
+ uint32 i;
+
+ if (!gfxClipHorizontalSpan(g, yy, &spanStart, &spanEnd))
+ continue;
+ if (rowX < spanStart)
+ {
+ rowWidth -= spanStart - rowX;
+ rowX = spanStart;
+ }
+ if ((rowX + rowWidth - 1) > spanEnd)
+ rowWidth = spanEnd - rowX + 1;
+ if (rowWidth <= 0)
+ continue;
+
+ to = base + yy * pitch + rowX;
+ for (i = (uint32)rowWidth; i != 0; i--)
+ *to++ = pixel;
+ }
+ if (!currentContext->fullDirty && !Graphics_isImageSurface(g))
+ markScreenDirty(currentContext, x, y, width, height);
+ return;
+ }
#ifdef __gl2_h_
if (Graphics_useOpenGL(g))
{
@@ -1108,7 +1276,7 @@ static void fillRect(Context currentContext, TCObject g, int32 x, int32 y, int32
{
x += Graphics_transX(g);
y += Graphics_transY(g);
- skia_setClip(Get_Clip(g));
+ skia_applyClip(g);
skia_fillRect(0, x, y, w, h, pixel | Graphics_alpha(g));
skia_restoreClip();
@@ -1319,13 +1487,16 @@ static void drawText(Context currentContext, TCObject g, JCharP text, int32 chrC
}
else
#endif
- for (row=row0; r < rmax; start+=rowWIB, r++,row += pitch) // draw each row
+ for (row=row0; r < rmax; start+=rowWIB, r++,row += pitch,y++) // draw each row
{
+ int32 rowStart, rowEnd;
+ if (!gfxClipHorizontalSpan(g, y, &rowStart, &rowEnd))
+ continue;
current = start;
ands = ands8 + (currentBit = startBit);
for (x=x0; x < xMax; x++)
{
- if ((*current & *ands++) != 0 && x >= xMin)
+ if ((*current & *ands++) != 0 && rowStart <= x && x <= rowEnd)
row[x] = foreColor;
if (++currentBit == 8) // finished this uint8?
{
@@ -1372,8 +1543,11 @@ static void drawText(Context currentContext, TCObject g, JCharP text, int32 chrC
}
else
#endif
- for (row=row0; r < rmax; start+=rowWIB, r++,row += pitch) // draw each row
+ for (row=row0; r < rmax; start+=rowWIB, r++,row += pitch,y++) // draw each row
{
+ int32 rowStart, rowEnd;
+ if (!gfxClipHorizontalSpan(g, y, &rowStart, &rowEnd))
+ continue;
current = start;
isLowNibble = isNibbleStartingLow;
i = (PixelConv*)&row[x0];
@@ -1381,7 +1555,7 @@ static void drawText(Context currentContext, TCObject g, JCharP text, int32 chrC
{
transparency = isLowNibble ? (*current++ & 0xF) : ((*current >> 4) & 0xF);
isLowNibble = !isLowNibble;
- if (transparency == 0 || x < xMin)
+ if (transparency == 0 || x < rowStart || x > rowEnd)
continue;
if (transparency == 0xF)
i->pixel = foreColor;
@@ -1445,14 +1619,17 @@ static void drawText(Context currentContext, TCObject g, JCharP text, int32 chrC
{
rowWIB = width+diffW;
start = alpha + istart * rowWIB;
- for (row=row0; r < rmax; start+=rowWIB, r++,row += pitch) // draw each row
+ for (row=row0; r < rmax; start+=rowWIB, r++,row += pitch,y++) // draw each row
{
+ int32 rowStart, rowEnd;
+ if (!gfxClipHorizontalSpan(g, y, &rowStart, &rowEnd))
+ continue;
current = start;
i = (PixelConv*)&row[x0];
for (x=x0; x < xMax; x++,i++)
{
transparency = *current++;
- if (transparency == 0 || x < xMin)
+ if (transparency == 0 || x < rowStart || x > rowEnd)
continue;
if (transparency == 0xFF)
i->pixel = foreColor;
@@ -1505,7 +1682,7 @@ static void drawText(Context currentContext, TCObject g, JCharP text, int32 chrC
x += Graphics_transX(g);
y += Graphics_transY(g);
- skia_setClip(Get_Clip(g));
+ skia_applyClip(g);
skia_drawText(0, text, chrCount * sizeof(JChar), x, y + fontSize, foreColor | Graphics_alpha(g), justifyWidth, fontSize, typefaceIndex);
skia_restoreClip();
@@ -1637,7 +1814,7 @@ static void ellipseDrawAndFill(Context currentContext, TCObject g, int32 xc, int
{
xc += Graphics_transX(g);
yc += Graphics_transY(g);
- skia_setClip(Get_Clip(g));
+ skia_applyClip(g);
skia_ellipseDrawAndFill(0, xc, yc, rx, ry, pc1 | Graphics_alpha(g), pc2 | Graphics_alpha(g), fill, gradient);
skia_restoreClip();
@@ -1836,7 +2013,7 @@ static void fillPolygon(Context currentContext, TCObject g, int32 *xPoints1, int
#else
static void fillPolygon(Context currentContext, TCObject g, int32 *xPoints1, int32 *yPoints1, int32 nPoints1, int32 *xPoints2, int32 *yPoints2, int32 nPoints2, int32 tx, int32 ty, Pixel c1, Pixel c2, bool gradient, bool isPie)
{
- skia_setClip(Get_Clip(g));
+ skia_applyClip(g);
skia_fillPolygon(0, xPoints1, yPoints1, nPoints1, Graphics_transX(g), Graphics_transY(g), c1 | Graphics_alpha(g), c2 | Graphics_alpha(g), gradient, isPie);
skia_restoreClip();
@@ -1864,17 +2041,18 @@ static void drawPolygon(Context currentContext, TCObject g, int32 *xPoints1, int
#endif
{
int32 i;
+ GfxPaint paint = gfxPaintFromColor(&pixel);
for (i=1; i < nPoints1; i++)
- drawLine(currentContext, g,tx + xPoints1[i-1], ty + yPoints1[i-1], tx + xPoints1[i], ty + yPoints1[i], pixel);
+ drawLine(currentContext, g,tx + xPoints1[i-1], ty + yPoints1[i-1], tx + xPoints1[i], ty + yPoints1[i], paint);
for (i=1; i < nPoints2; i++)
- drawLine(currentContext, g,tx + xPoints2[i-1], ty + yPoints2[i-1], tx + xPoints2[i], ty + yPoints2[i], pixel);
+ drawLine(currentContext, g,tx + xPoints2[i-1], ty + yPoints2[i-1], tx + xPoints2[i], ty + yPoints2[i], paint);
}
}
}
#else
static void drawPolygon(Context currentContext, TCObject g, int32 *xPoints1, int32 *yPoints1, int32 nPoints1, int32 *xPoints2, int32 *yPoints2, int32 nPoints2, int32 tx, int32 ty, Pixel pixel)
{
- skia_setClip(Get_Clip(g));
+ skia_applyClip(g);
skia_drawPolygon(0, xPoints1, yPoints1, nPoints1, Graphics_transX(g), Graphics_transY(g), pixel | Graphics_alpha(g));
skia_restoreClip();
@@ -1891,7 +2069,7 @@ static void arcPiePointDrawAndFill(Context currentContext, TCObject g, int32 xc,
{
xc += Graphics_transX(g);
yc += Graphics_transY(g);
- skia_setClip(Get_Clip(g));
+ skia_applyClip(g);
skia_arcPiePointDrawAndFill(0, xc, yc, rx, ry, startAngle, endAngle, c | Graphics_alpha(g), c2 | Graphics_alpha(g), fill, pie, gradient);
skia_restoreClip();
@@ -1908,7 +2086,7 @@ static void arcPiePointDrawAndFill(Context currentContext, TCObject g, int32 xc,
TCObject *yPointsObj = &Graphics_yPoints(g);
int32 *xPoints = *xPointsObj ? (int32*)ARRAYOBJ_START(*xPointsObj) : null;
int32 *yPoints = *yPointsObj ? (int32*)ARRAYOBJ_START(*yPointsObj) : null;
- int32 clipFactor = Graphics_minX(g) * 1000000000 + Graphics_maxX(g) * 10000000 + Graphics_minY(g) * 100000 + Graphics_maxY(g);
+ int32 clipFactor = 0;
bool sameClipFactor = Graphics_lastClipFactor(g) == clipFactor;
if (rx < 0 || ry < 0) // guich@501_13
@@ -2150,7 +2328,10 @@ static void arcPiePointDrawAndFill(Context currentContext, TCObject g, int32 xc,
yPoints[endIndex] = oldY1;
#ifdef ANDROID
if (!gradient && endAngle == 360)
- drawLine(currentContext,g, xc,yc, xc+xPoints[endIndex-1], yc+yPoints[endIndex-1], c);
+ {
+ Pixel lineColor = c;
+ drawLine(currentContext,g, xc,yc, xc+xPoints[endIndex-1], yc+yPoints[endIndex-1], gfxPaintFromColor(&lineColor));
+ }
#endif
}
}
@@ -2197,7 +2378,7 @@ static void drawRoundRect(Context currentContext, TCObject g, int32 x, int32 y,
{
x += Graphics_transX(g);
y += Graphics_transY(g);
- skia_setClip(Get_Clip(g));
+ skia_applyClip(g);
skia_drawRoundRect(0, x, y, w, h, r, c | Graphics_alpha(g));
skia_restoreClip();
@@ -2205,6 +2386,78 @@ static void drawRoundRect(Context currentContext, TCObject g, int32 x, int32 y,
}
#endif
+////////////////////////////////////////////////////////////////////////////
+#ifndef SKIA_H
+static void drawRRect(Context currentContext, TCObject g, int32 x, int32 y, int32 w, int32 h, const double *radii, Pixel c, bool filled)
+{
+ int32 top = y;
+ int32 bottom = y + h - 1;
+ int32 yy;
+ int32 start, end;
+ int32 previousStart = 0x7FFFFFFF;
+ int32 previousEnd = 0x7FFFFFFF;
+ Pixel lineColor = c;
+ GfxPaint paint = gfxPaintFromColor(&lineColor);
+
+ if (w <= 0 || h <= 0)
+ return;
+
+ if (radii == null)
+ {
+ if (filled)
+ fillRect(currentContext, g, x, y, w, h, c);
+ else
+ drawRect(currentContext, g, x, y, w, h, c);
+ return;
+ }
+
+ for (yy = top; yy <= bottom; yy++)
+ {
+ if (!gfxComputeRRectSpan(x, y, w, h, radii, yy, &start, &end))
+ continue;
+
+ if (filled)
+ drawLine(currentContext, g, start, yy, end, yy, paint);
+ else
+ {
+ if (previousStart == 0x7FFFFFFF)
+ drawLine(currentContext, g, start, yy, end, yy, paint);
+ else
+ {
+ if (start < previousStart)
+ drawLine(currentContext, g, start, yy, previousStart, yy, paint);
+ else if (start > previousStart)
+ drawLine(currentContext, g, previousStart, yy - 1, start, yy - 1, paint);
+
+ if (end > previousEnd)
+ drawLine(currentContext, g, previousEnd, yy, end, yy, paint);
+ else if (end < previousEnd)
+ drawLine(currentContext, g, end, yy - 1, previousEnd, yy - 1, paint);
+ }
+ setPixel(currentContext, g, start, yy, c);
+ setPixel(currentContext, g, end, yy, c);
+ }
+
+ previousStart = start;
+ previousEnd = end;
+ }
+
+ if (!filled && previousStart != 0x7FFFFFFF)
+ drawLine(currentContext, g, previousStart, bottom, previousEnd, bottom, paint);
+}
+#else
+static void drawRRect(Context currentContext, TCObject g, int32 x, int32 y, int32 w, int32 h, const double *radii, Pixel c, bool filled)
+{
+ x += Graphics_transX(g);
+ y += Graphics_transY(g);
+ skia_applyClip(g);
+ skia_drawRRect(0, x, y, w, h, radii, c | Graphics_alpha(g), filled);
+ skia_restoreClip();
+
+ markDirty(currentContext, g, x, y, w, h);
+}
+#endif
+
#ifndef SKIA_H
////////////////////////////////////////////////////////////////////////////
static void setPixelA(Context currentContext, TCObject g, int32 x, int32 y, PixelConv color, int32 alpha);
@@ -2214,6 +2467,8 @@ static void fillRoundRect(Context currentContext, TCObject g, int32 xx, int32 yy
{
int32 px1,px2,py1,py2,xm,ym,x,y=0, i, x2, e2, err;
PixelConv color;
+ Pixel lineColor = c;
+ GfxPaint gfxPaint = gfxPaintFromColor(&lineColor);
if (r > (width/2) || r > (height/2)) r = min32(width/2,height/2); // guich@200b4_6: correct bug that crashed the device.
x = -r;
@@ -2235,8 +2490,8 @@ static void fillRoundRect(Context currentContext, TCObject g, int32 xx, int32 yy
{
i = 255 - 255 * abs(err - 2 * (x + y) - 2) / r;
- drawLine(currentContext, g, px1+x+1,py1-y,px2-x-1,py1-y,c);
- drawLine(currentContext, g, px1+x+1,py2+y,px2-x-1,py2+y,c);
+ drawLine(currentContext, g, px1+x+1,py1-y,px2-x-1,py1-y,gfxPaint);
+ drawLine(currentContext, g, px1+x+1,py2+y,px2-x-1,py2+y,gfxPaint);
if (i < 256 && i > 0)
{
@@ -2278,7 +2533,7 @@ static void fillRoundRect(Context currentContext, TCObject g, int32 x, int32 y,
{
x += Graphics_transX(g);
y += Graphics_transY(g);
- skia_setClip(Get_Clip(g));
+ skia_applyClip(g);
skia_fillRoundRect(0, x, y, w, h, r, c | Graphics_alpha(g));
skia_restoreClip();
@@ -2897,6 +3152,7 @@ static void drawRoundGradient(Context currentContext, TCObject g, int32 startX,
for (i = 0; i < numSteps; i++)
{
+ GfxPaint paint;
if (hasRadius)
{
leftOffset = rightOffset = 0;
@@ -2917,19 +3173,20 @@ static void drawRoundGradient(Context currentContext, TCObject g, int32 startX,
if (rightOffset < 0) rightOffset = 0;
}
p = makePixel(red >> 16, green >> 16, blue >> 16);
+ paint = gfxPaintFromColor(&p);
if (!optimize || leftOffset != 0 || rightOffset != 0)
{
if (vertical)
{
int32 fc = p;
- drawLine(currentContext, g, startX + leftOffset, startY + i, endX - rightOffset, startY + i, p);
+ drawLine(currentContext, g, startX + leftOffset, startY + i, endX - rightOffset, startY + i, paint);
if (drawFadedPixels && rightOffset != 0) // since there's no fading of pixels in opengl, we can safely ignore this
drawFadedPixel(currentContext, g, endX - rightOffset + 1, startY + i, fc);
if (drawFadedPixels && leftOffset != 0)
drawFadedPixel(currentContext, g, startX + leftOffset - 1, startY + i, fc);
}
else
- drawLine(currentContext, g, startX + i, startY + leftOffset, startX + i, endY - rightOffset, p);
+ drawLine(currentContext, g, startX + i, startY + leftOffset, startX + i, endY - rightOffset, paint);
}
if (stage < 2) // find starting and ending colors
{
@@ -2977,7 +3234,7 @@ static void drawRoundGradient(Context currentContext, TCObject g, int32 startX,
startY += Graphics_transY(g);
endX += Graphics_transX(g);
endY += Graphics_transY(g);
- skia_setClip(Get_Clip(g));
+ skia_applyClip(g);
skia_drawRoundGradient(0, startX, startY, endX, endY, topLeftRadius, topRightRadius, bottomLeftRadius, bottomRightRadius, startColor | Graphics_alpha(g), endColor | Graphics_alpha(g), vertical);
skia_restoreClip();
@@ -2998,9 +3255,10 @@ static int getsetRGB(Context currentContext, TCObject g, TCObject dataObj, int32
if (translateAndClip(g, &x, &y, &w, &h))
{
Pixel* data = ((Pixel*)ARRAYOBJ_START(dataObj)) + offset;
- int32 inc = Graphics_pitch(g), count = w * h;
- Pixel* pixels = getGraphicsPixels(g) + y * inc + x;
+ int32 inc = Graphics_pitch(g), count = 0;
+ Pixel* pixels = getGraphicsPixels(g);
bool markDirty = !currentContext->fullDirty && !Graphics_isImageSurface(g);
+ int32 yy;
#if 0//def __gl2_h_
currentContext->fullDirty |= markDirty;
if (isGet && Graphics_useOpenGL(g))
@@ -3008,15 +3266,41 @@ static int getsetRGB(Context currentContext, TCObject g, TCObject dataObj, int32
else
#endif
if (isGet)
- for (; h-- > 0; pixels += inc, data += w)
- xmemmove(data, pixels, w<<2);
+ for (yy = y; yy < y + h; yy++)
+ {
+ int32 start, end, width;
+ if (!gfxClipHorizontalSpan(g, yy, &start, &end))
+ continue;
+ if (start < x)
+ start = x;
+ if (end > (x + w - 1))
+ end = x + w - 1;
+ if (end < start)
+ continue;
+ width = end - start + 1;
+ xmemmove(data, pixels + yy * inc + start, width << 2);
+ data += width;
+ count += width;
+ }
else
- for (; h-- > 0; pixels += inc, data += w)
+ for (yy = y; yy < y + h; yy++)
{
- xmemmove(pixels, data, w<<2);
+ int32 start, end, width;
+ if (!gfxClipHorizontalSpan(g, yy, &start, &end))
+ continue;
+ if (start < x)
+ start = x;
+ if (end > (x + w - 1))
+ end = x + w - 1;
+ if (end < start)
+ continue;
+ width = end - start + 1;
+ xmemmove(pixels + yy * inc + start, data, width << 2);
+ data += width;
+ count += width;
#ifndef __gl2_h_
if (markDirty)
- markScreenDirty(currentContext, x, y++, w, 1);
+ markScreenDirty(currentContext, start, yy, width, 1);
#endif
}
return count;
diff --git a/TotalCrossVM/src/nm/ui/android/skia.cpp b/TotalCrossVM/src/nm/ui/android/skia.cpp
index 2262e0f6c..82426cee5 100644
--- a/TotalCrossVM/src/nm/ui/android/skia.cpp
+++ b/TotalCrossVM/src/nm/ui/android/skia.cpp
@@ -72,6 +72,7 @@
#include "include/core/SkImageInfo.h"
#include "include/core/SkImageEncoder.h"
#include "include/core/SkPath.h"
+#include "include/core/SkRRect.h"
#include "include/effects/SkGradientShader.h"
#include "include/core/SkTextBlob.h"
@@ -92,6 +93,9 @@
#include
#include