Merge pull request #492 from dayllenger/shadows

Box shadows
This commit is contained in:
Vadim Lopatin 2017-10-16 13:27:45 +03:00 committed by GitHub
commit 29ff80b049
3 changed files with 215 additions and 25 deletions

View File

@ -858,20 +858,26 @@ class DrawBuf : RefCountedObject {
alias DrawBufRef = Ref!DrawBuf;
/// RAII setting/restoring of clip rectangle
/// RAII setting/restoring of a DrawBuf clip rectangle
struct ClipRectSaver {
private DrawBuf _buf;
private Rect _oldClipRect;
private uint _oldAlpha;
/// apply (intersect) new clip rectangle and alpha to draw buf; restore
this(DrawBuf buf, ref Rect newClipRect, uint newAlpha = 0) {
/// apply (intersect) new clip rectangle and alpha to draw buf
/// set `intersect` parameter to `false`, if you want to draw something outside of the widget
this(DrawBuf buf, ref Rect newClipRect, uint newAlpha = 0, bool intersect = true) {
_buf = buf;
_oldClipRect = buf.clipRect;
_oldAlpha = buf.alpha;
buf.intersectClipRect(newClipRect);
if (intersect)
buf.intersectClipRect(newClipRect);
else
buf.clipRect = newClipRect;
if (newAlpha)
buf.addAlpha(newAlpha);
}
/// restore previous clip rectangle
~this() {
_buf.clipRect = _oldClipRect;
_buf.alpha = _oldAlpha;
@ -1224,7 +1230,6 @@ class ColorDrawBufBase : DrawBuf {
row[x] = blendARGB(row[x], color, alpha);
}
}
}
class GrayDrawBuf : DrawBuf {
@ -1469,6 +1474,7 @@ class GrayDrawBuf : DrawBuf {
class ColorDrawBuf : ColorDrawBufBase {
uint[] _buf;
/// create ARGB8888 draw buf of specified width and height
this(int width, int height) {
resize(width, height);
@ -1525,11 +1531,13 @@ class ColorDrawBuf : ColorDrawBufBase {
pixel ^= 0xFF000000;
}
}
override uint * scanLine(int y) {
if (y >= 0 && y < _dy)
return _buf.ptr + _dx * y;
return null;
}
override void resize(int width, int height) {
if (_dx == width && _dy == height)
return;
@ -1538,6 +1546,7 @@ class ColorDrawBuf : ColorDrawBufBase {
_buf.length = _dx * _dy;
resetClipping();
}
override void fill(uint color) {
if (hasClipping) {
fillRect(_clipRect, color);
@ -1548,6 +1557,7 @@ class ColorDrawBuf : ColorDrawBufBase {
foreach(i; 0 .. len)
p[i] = color;
}
override DrawBuf transformColors(ref ColorTransform transform) {
if (transform.empty)
return this;
@ -1572,6 +1582,71 @@ class ColorDrawBuf : ColorDrawBufBase {
}
return res;
}
/// Apply Gaussian blur on the image
void blur(uint blurSize) {
if (blurSize == 0)
return; // trivial case
// utility functions to get and set color
float[4] get(uint[] buf, uint x, uint y) {
uint c = buf[x + y * _dx];
float a = 255 - (c >> 24);
float r = (c >> 16) & 0xFF;
float g = (c >> 8) & 0xFF;
float b = (c >> 0) & 0xFF;
return [r, g, b, a];
}
void set(uint[] buf, uint x, uint y, float[4] c) {
buf[x + y * _dx] = makeRGBA(c[0], c[1], c[2], 255 - c[3]);
}
import std.algorithm : max, min;
import std.math;
// Gaussian function
float weight(in float x, in float sigma) pure nothrow {
enum inv_sqrt_2pi = 1 / sqrt(2 * PI);
return exp(- x ^^ 2 / (2 * sigma ^^ 2)) * inv_sqrt_2pi / sigma;
}
void blurOneDimension(uint[] bufIn, uint[] bufOut, uint blurSize, bool horizontally) {
float sigma = blurSize > 2 ? blurSize / 3.0 : blurSize / 2.0;
foreach (x; 0 .. _dx) {
foreach (y; 0 .. _dy) {
float[4] c;
c[] = 0;
float sum = 0;
foreach (int i; 1 .. blurSize + 1) {
float[4] c1 = get(bufIn,
horizontally ? min(x + i, _dx - 1) : x,
horizontally ? y : min(y + i, _dy - 1)
);
float[4] c2 = get(bufIn,
horizontally ? max(x - i, 0) : x,
horizontally ? y : max(y - i, 0)
);
float w = weight(i, sigma);
c[] += (c1[] + c2[]) * w;
sum += 2 * w;
}
c[] += get(bufIn, x, y)[] * (1 - sum);
set(bufOut, x, y, c);
}
}
}
// intermediate buffer for image
uint[] tmpbuf;
tmpbuf.length = _buf.length;
// do horizontal blur
blurOneDimension(_buf, tmpbuf, blurSize, true);
// then do vertical blur
blurOneDimension(tmpbuf, _buf, blurSize, false);
}
}

View File

@ -408,6 +408,71 @@ class BorderDrawable : Drawable {
}
deprecated alias FrameDrawable = BorderDrawable;
/// box shadows, can be blurred
class BoxShadowDrawable : Drawable {
protected int _offsetX;
protected int _offsetY;
protected int _blurSize;
protected uint _color;
protected Ref!ColorDrawBuf texture;
this(int offsetX, int offsetY, uint blurSize = 0, uint color = 0x0) {
_offsetX = offsetX;
_offsetY = offsetY;
_blurSize = blurSize;
_color = color;
// now create a texture which will contain the shadow
uint size = 4 * blurSize + 1;
texture = new ColorDrawBuf(size, size); // TODO: get from/put to cache
// clear
texture.fill(color | 0xFF000000);
// draw a square in center of the texture
texture.fillRect(Rect(blurSize, blurSize, size - blurSize, size - blurSize), color);
// blur the square
texture.blur(blurSize);
}
override void drawTo(DrawBuf buf, Rect rc, uint state = 0, int tilex0 = 0, int tiley0 = 0) {
// this is a size of blurred part
uint b = _blurSize + _blurSize / 2 + 1;
// move and expand the shadow
rc.left += _offsetX - b;
rc.top += _offsetY - b;
rc.right += _offsetX + b;
rc.bottom += _offsetY + b;
// apply new clipping to the DrawBuf to draw outside of the widget
auto saver = ClipRectSaver(buf, rc, 0, false);
if (_blurSize > 0) {
// Manual nine-patch
uint w = texture.width;
uint h = texture.height;
buf.drawFragment(rc.left, rc.top, texture, Rect(0, 0, b, b)); // top left
buf.drawRescaled(Rect(rc.left + b, rc.top, rc.right - b, rc.top + b), texture, Rect(b, 0, w - b, b)); // top center
buf.drawFragment(rc.right - b, rc.top, texture, Rect(w - b, 0, w, b)); // top right
buf.drawRescaled(Rect(rc.left, rc.top + b, rc.left + b, rc.bottom - b), texture, Rect(0, b, b, h - b)); // middle left
buf.drawRescaled(Rect(rc.left + b, rc.top + b, rc.right - b, rc.bottom - b), texture, Rect(b, b, w - b, h - b)); // middle center
buf.drawRescaled(Rect(rc.right - b, rc.top + b, rc.right, rc.bottom - b), texture, Rect(w - b, b, w, h - b)); // middle right
buf.drawFragment(rc.left, rc.bottom - b, texture, Rect(0, h - b, b, h)); // bottom left
buf.drawRescaled(Rect(rc.left + b, rc.bottom - b, rc.right - b, rc.bottom), texture, Rect(b, h - b, w - b, h)); // bottom center
buf.drawFragment(rc.right - b, rc.bottom - b, texture, Rect(w - b, h - b, w, h)); // bottom right
// debug
//~ buf.drawFragment(rc.left, rc.top, texture, Rect(0, 0, w, h));
} else {
buf.fillRect(rc, _color);
}
}
@property override int width() { return 1; }
@property override int height() { return 1; }
}
enum DimensionUnits {
pixels,
points,
@ -491,32 +556,42 @@ static if (BACKEND_CONSOLE) {
/// decode solid color / gradient / border drawable from string like #AARRGGBB, e.g. #5599AA
///
/// SolidFillDrawable: #AARRGGBB - e.g. #8090A0 or #80ffffff
/// GradientDrawable: #linear,Ndeg,#firstColor,#secondColor
/// BorderDrawable: #borderColor,borderWidth[,#middleColor]
/// or #borderColor,leftBorderWidth,topBorderWidth,rightBorderWidth,bottomBorderWidth[,#middleColor]
/// e.g. #000000,2,#C0FFFFFF - black border of width 2 with 75% transparent white middle
/// e.g. #0000FF,2,3,4,5,#FFFFFF - blue border with left,top,right,bottom borders of width 2,3,4,5 and white inner area
/// GradientDrawable: #linear,Ndeg,firstColor,secondColor
/// BorderDrawable: #border,borderColor,borderWidth[,middleColor]
/// or #border,borderColor,leftBorderWidth,topBorderWidth,rightBorderWidth,bottomBorderWidth[,middleColor]
/// e.g. #border,#000000,2,#C0FFFFFF - black border of width 2 with 75% transparent white middle
/// e.g. #border,#0000FF,2,3,4,5,#FFFFFF - blue border with left,top,right,bottom borders of width 2,3,4,5 and white inner area
static Drawable createColorDrawable(string s) {
Log.d("creating color drawable ", s);
enum DrawableType { SolidColor, LinearGradient, Border }
enum DrawableType { SolidColor, LinearGradient, Border, BoxShadow }
auto type = DrawableType.SolidColor;
string[] items = s.split(',');
uint[] values;
foreach (i, item; items) {
if (item == "#linear")
int[] ivalues;
if (items.length != 0) {
if (items[0] == "#linear")
type = DrawableType.LinearGradient;
else if (item.startsWith("#"))
values ~= decodeHexColor(item);
else if (item.endsWith("deg"))
values ~= decodeAngle(item);
else {
values ~= decodeDimension(item);
else if (items[0] == "#border")
type = DrawableType.Border;
else if (items[0] == "#box-shadow")
type = DrawableType.BoxShadow;
else if (items[0].startsWith("#"))
values ~= decodeHexColor(items[0]);
foreach (i, item; items[1 .. $]) {
if (item.startsWith("#"))
values ~= decodeHexColor(item);
else if (item.endsWith("deg"))
values ~= decodeAngle(item);
else if (type == DrawableType.BoxShadow) // offsets may be negative
ivalues ~= item.startsWith("-") ? -decodeDimension(item) : decodeDimension(item);
else
values ~= decodeDimension(item);
if (i >= 6)
break;
}
if (i >= 6)
break;
}
if (type == DrawableType.SolidColor && values.length == 1) // only color #AARRGGBB
@ -532,6 +607,13 @@ static Drawable createColorDrawable(string s) {
return new BorderDrawable(values[0], Rect(values[1], values[2], values[3], values[4]));
else if (values.length == 6) // border color, border widths for left,top,right,bottom, inner area color - #AARRGGBB,NNleft,NNtop,NNright,NNbottom,#AARRGGBB
return new BorderDrawable(values[0], Rect(values[1], values[2], values[3], values[4]), values[5]);
} else if (type == DrawableType.BoxShadow) {
if (ivalues.length == 2 && values.length == 0) // shadow X and Y offsets
return new BoxShadowDrawable(ivalues[0], ivalues[1]);
else if (ivalues.length == 3 && values.length == 0) // shadow offsets and blur size
return new BoxShadowDrawable(ivalues[0], ivalues[1], ivalues[2]);
else if (ivalues.length == 3 && values.length == 1) // shadow offsets, blur size and color
return new BoxShadowDrawable(ivalues[0], ivalues[1], ivalues[2], values[0]);
}
Log.e("Invalid drawable string format: ", s);
return new EmptyDrawable(); // invalid format - just return empty drawable
@ -1133,19 +1215,22 @@ class StateDrawable : Drawable {
/// Drawable which allows to combine together background image, gradient, borders, box shadows, etc.
class CombinedDrawable : Drawable {
DrawableRef boxShadow;
DrawableRef background;
DrawableRef border;
this(uint backgroundColor, string backgroundImageId, string borderDescription) {
this(uint backgroundColor, string backgroundImageId, string borderDescription, string boxShadowDescription) {
boxShadow = boxShadowDescription !is null ? drawableCache.get("#box-shadow," ~ boxShadowDescription) : new EmptyDrawable;
background =
(backgroundImageId !is null) ? drawableCache.get(backgroundImageId) :
(!backgroundColor.isFullyTransparentColor) ? new SolidFillDrawable(backgroundColor) : null;
if (background is null)
background = new EmptyDrawable;
border = borderDescription !is null ? drawableCache.get(borderDescription) : new EmptyDrawable;
border = borderDescription !is null ? drawableCache.get("#border," ~ borderDescription) : new EmptyDrawable;
}
override void drawTo(DrawBuf buf, Rect rc, uint state = 0, int tilex0 = 0, int tiley0 = 0) {
boxShadow.drawTo(buf, rc, state, tilex0, tiley0);
// make background image smaller to fit borders
Rect backrc = rc;
backrc.left += border.padding.left;

View File

@ -324,6 +324,7 @@ protected:
uint _alpha;
string _fontFace;
string _backgroundImageId;
string _boxShadow;
string _border;
Rect _padding;
Rect _margins;
@ -413,8 +414,10 @@ public:
return (cast(Style)this)._backgroundDrawable;
string image = backgroundImageId;
uint color = backgroundColor;
if (border !is null) {
(cast(Style)this)._backgroundDrawable = new CombinedDrawable(color, image, border);
string borders = border;
string shadows = boxShadow;
if (borders !is null || shadows !is null) {
(cast(Style)this)._backgroundDrawable = new CombinedDrawable(color, image, borders, shadows);
} else if (image !is null) {
(cast(Style)this)._backgroundDrawable = drawableCache.get(image);
} else {
@ -530,6 +533,15 @@ public:
return parentStyle.fontSize;
}
/// box shadow
@property string boxShadow() const {
if (_boxShadow !is null)
return _boxShadow;
else {
return parentStyle.boxShadow;
}
}
/// border
@property string border() const {
if (_border !is null)
@ -791,6 +803,12 @@ public:
return this;
}
@property Style boxShadow(string s) {
_boxShadow = s;
_backgroundDrawable.clear();
return this;
}
@property Style border(string s) {
_border = s;
_backgroundDrawable.clear();
@ -908,6 +926,7 @@ public:
res._alpha = _alpha;
res._fontFace = _fontFace;
res._backgroundImageId = _backgroundImageId;
res._boxShadow = _boxShadow;
res._border = _border;
res._padding = _padding;
res._margins = _margins;
@ -1038,6 +1057,10 @@ class Theme : Style {
@property override string backgroundImageId() const {
return _backgroundImageId;
}
/// box shadow
@property override string boxShadow() const {
return _boxShadow;
}
/// border
@property override string border() const {
return _border;
@ -1527,6 +1550,11 @@ string sanitizeBorderProperty(string s) pure {
return cast(string)res;
}
/// remove superfluous space characters from a box shadow property
string sanitizeBoxShadowProperty(string s) pure {
return sanitizeBorderProperty(s);
}
/// load style attributes from XML element
bool loadStyleAttributes(Style style, Element elem, bool allowStates) {
//Log.d("Theme: loadStyleAttributes ", style.id, " ", elem.tag.attr);
@ -1542,6 +1570,8 @@ bool loadStyleAttributes(Style style, Element elem, bool allowStates) {
style.padding = decodeRect(elem.tag.attr["padding"]);
if ("border" in elem.tag.attr)
style.border = sanitizeBorderProperty(elem.tag.attr["border"]);
if ("boxShadow" in elem.tag.attr)
style.boxShadow = sanitizeBoxShadowProperty(elem.tag.attr["boxShadow"]);
if ("align" in elem.tag.attr)
style.alignment = decodeAlignment(elem.tag.attr["align"]);
if ("minWidth" in elem.tag.attr)