Add support for 9-sliced backgrounds (#8600)

9-slice textures are commonly used in GUIs to allow scaling them to match any resolution without distortion.

https://en.wikipedia.org/wiki/9-slice_scaling
This commit is contained in:
rubenwardy 2019-06-22 15:03:54 +01:00 committed by SmallJoker
parent 4e3c1916f7
commit 429a989648
5 changed files with 137 additions and 13 deletions

@ -2034,13 +2034,26 @@ Elements
### `background[<X>,<Y>;<W>,<H>;<texture name>]` ### `background[<X>,<Y>;<W>,<H>;<texture name>]`
* Use a background. Inventory rectangles are not drawn then.
* Example for formspec 8x4 in 16x resolution: image shall be sized * Example for formspec 8x4 in 16x resolution: image shall be sized
8 times 16px times 4 times 16px. 8 times 16px times 4 times 16px.
### `background[<X>,<Y>;<W>,<H>;<texture name>;<auto_clip>]` ### `background[<X>,<Y>;<W>,<H>;<texture name>;<auto_clip>]`
* Use a background. Inventory rectangles are not drawn then. * Example for formspec 8x4 in 16x resolution:
image shall be sized 8 times 16px times 4 times 16px
* If `auto_clip` is `true`, the background is clipped to the formspec size
(`x` and `y` are used as offset values, `w` and `h` are ignored)
### `background[<X>,<Y>;<W>,<H>;<texture name>;<auto_clip>;<middle>]`
* 9-sliced background. See https://en.wikipedia.org/wiki/9-slice_scaling
* Middle is a rect which defines the middle of the 9-slice.
* `x` - The middle will be x pixels from all sides.
* `x,y` - The middle will be x pixels from the horizontal and y from the vertical.
* `x,y,x2,y2` - The middle will start at x,y, and end at x2, y2. Negative x2 and y2 values
will be added to the width and height of the texture, allowing it to be used as the
distance from the far end.
* All numbers in middle are integers.
* Example for formspec 8x4 in 16x resolution: * Example for formspec 8x4 in 16x resolution:
image shall be sized 8 times 16px times 4 times 16px image shall be sized 8 times 16px times 4 times 16px
* If `auto_clip` is `true`, the background is clipped to the formspec size * If `auto_clip` is `true`, the background is clipped to the formspec size

@ -167,3 +167,62 @@ void draw2DImageFilterScaled(video::IVideoDriver *driver, video::ITexture *txr,
driver->draw2DImage(scaled, destrect, mysrcrect, cliprect, colors, usealpha); driver->draw2DImage(scaled, destrect, mysrcrect, cliprect, colors, usealpha);
} }
void draw2DImage9Slice(video::IVideoDriver *driver, video::ITexture *texture,
const core::rect<s32> &rect, const core::rect<s32> &middle)
{
const video::SColor color(255,255,255,255);
const video::SColor colors[] = {color,color,color,color};
auto originalSize = texture->getOriginalSize();
core::vector2di lowerRightOffset = core::vector2di(originalSize.Width, originalSize.Height) - middle.LowerRightCorner;
for (int y = 0; y < 3; ++y) {
for (int x = 0; x < 3; ++x) {
core::rect<s32> src({0, 0}, originalSize);
core::rect<s32> dest = rect;
switch (x) {
case 0:
dest.LowerRightCorner.X = rect.UpperLeftCorner.X + middle.UpperLeftCorner.X;
src.LowerRightCorner.X = middle.UpperLeftCorner.X;
break;
case 1:
dest.UpperLeftCorner.X += middle.UpperLeftCorner.X;
dest.LowerRightCorner.X -= lowerRightOffset.X;
src.UpperLeftCorner.X = middle.UpperLeftCorner.X;
src.LowerRightCorner.X = middle.LowerRightCorner.X;
break;
case 2:
dest.UpperLeftCorner.X = rect.LowerRightCorner.X - lowerRightOffset.X;
src.UpperLeftCorner.X = middle.LowerRightCorner.X;
break;
}
switch (y) {
case 0:
dest.LowerRightCorner.Y = rect.UpperLeftCorner.Y + middle.UpperLeftCorner.Y;
src.LowerRightCorner.Y = middle.UpperLeftCorner.Y;
break;
case 1:
dest.UpperLeftCorner.Y += middle.UpperLeftCorner.Y;
dest.LowerRightCorner.Y -= lowerRightOffset.Y;
src.UpperLeftCorner.Y = middle.UpperLeftCorner.Y;
src.LowerRightCorner.Y = middle.LowerRightCorner.Y;
break;
case 2:
dest.UpperLeftCorner.Y = rect.LowerRightCorner.Y - lowerRightOffset.Y;
src.UpperLeftCorner.Y = middle.LowerRightCorner.Y;
break;
}
draw2DImageFilterScaled(driver, texture, dest,
src,
NULL/*&AbsoluteClippingRect*/, colors, true);
}
}
}

@ -48,3 +48,9 @@ void draw2DImageFilterScaled(video::IVideoDriver *driver, video::ITexture *txr,
const core::rect<s32> &destrect, const core::rect<s32> &srcrect, const core::rect<s32> &destrect, const core::rect<s32> &srcrect,
const core::rect<s32> *cliprect = 0, const video::SColor *const colors = 0, const core::rect<s32> *cliprect = 0, const video::SColor *const colors = 0,
bool usealpha = false); bool usealpha = false);
/*
* 9-slice / segment drawing
*/
void draw2DImage9Slice(video::IVideoDriver *driver, video::ITexture *texture,
const core::rect<s32> &rect, const core::rect<s32> &middle);

@ -653,9 +653,8 @@ void GUIFormSpecMenu::parseBackground(parserData* data, const std::string &eleme
{ {
std::vector<std::string> parts = split(element,';'); std::vector<std::string> parts = split(element,';');
if (((parts.size() == 3) || (parts.size() == 4)) || if ((parts.size() >= 3 && parts.size() <= 5) ||
((parts.size() > 4) && (m_formspec_version > FORMSPEC_API_VERSION))) (parts.size() > 5 && m_formspec_version > FORMSPEC_API_VERSION)) {
{
std::vector<std::string> v_pos = split(parts[0],','); std::vector<std::string> v_pos = split(parts[0],',');
std::vector<std::string> v_geom = split(parts[1],','); std::vector<std::string> v_geom = split(parts[1],',');
std::string name = unescape_string(parts[2]); std::string name = unescape_string(parts[2]);
@ -672,16 +671,37 @@ void GUIFormSpecMenu::parseBackground(parserData* data, const std::string &eleme
geom.Y = stof(v_geom[1]) * spacing.Y; geom.Y = stof(v_geom[1]) * spacing.Y;
bool clip = false; bool clip = false;
if (parts.size() == 4 && is_yes(parts[3])) { if (parts.size() >= 4 && is_yes(parts[3])) {
pos.X = stoi(v_pos[0]); //acts as offset pos.X = stoi(v_pos[0]); //acts as offset
pos.Y = stoi(v_pos[1]); //acts as offset pos.Y = stoi(v_pos[1]); //acts as offset
clip = true; clip = true;
} }
core::rect<s32> middle;
if (parts.size() >= 5) {
std::vector<std::string> v_middle = split(parts[4], ',');
if (v_middle.size() == 1) {
s32 x = stoi(v_middle[0]);
middle.UpperLeftCorner = core::vector2di(x, x);
middle.LowerRightCorner = core::vector2di(-x, -x);
} else if (v_middle.size() == 2) {
s32 x = stoi(v_middle[0]);
s32 y = stoi(v_middle[1]);
middle.UpperLeftCorner = core::vector2di(x, y);
middle.LowerRightCorner = core::vector2di(-x, -y);
// `-x` is interpreted as `w - x`
} else if (v_middle.size() == 4) {
middle.UpperLeftCorner = core::vector2di(stoi(v_middle[0]), stoi(v_middle[1]));
middle.LowerRightCorner = core::vector2di(stoi(v_middle[2]), stoi(v_middle[3]));
} else {
warningstream << "Invalid rectangle given to middle param of background[] element" << std::endl;
}
}
if (!data->explicit_size && !clip) if (!data->explicit_size && !clip)
warningstream << "invalid use of unclipped background without a size[] element" << std::endl; warningstream << "invalid use of unclipped background without a size[] element" << std::endl;
m_backgrounds.emplace_back(name, pos, geom, clip); m_backgrounds.emplace_back(name, pos, geom, middle, clip);
return; return;
} }
@ -2514,6 +2534,8 @@ void GUIFormSpecMenu::drawMenu()
core::rect<s32> imgrect(0, 0, spec.geom.X, spec.geom.Y); core::rect<s32> imgrect(0, 0, spec.geom.X, spec.geom.Y);
// Image rectangle on screen // Image rectangle on screen
core::rect<s32> rect = imgrect + spec.pos; core::rect<s32> rect = imgrect + spec.pos;
// Middle rect for 9-slicing
core::rect<s32> middle = spec.middle;
if (spec.clip) { if (spec.clip) {
core::dimension2d<s32> absrec_size = AbsoluteRect.getSize(); core::dimension2d<s32> absrec_size = AbsoluteRect.getSize();
@ -2523,12 +2545,23 @@ void GUIFormSpecMenu::drawMenu()
AbsoluteRect.UpperLeftCorner.Y + absrec_size.Height + spec.pos.Y); AbsoluteRect.UpperLeftCorner.Y + absrec_size.Height + spec.pos.Y);
} }
const video::SColor color(255,255,255,255); if (middle.getArea() == 0) {
const video::SColor colors[] = {color,color,color,color}; const video::SColor color(255, 255, 255, 255);
const video::SColor colors[] = {color, color, color, color};
draw2DImageFilterScaled(driver, texture, rect, draw2DImageFilterScaled(driver, texture, rect,
core::rect<s32>(core::position2d<s32>(0,0), core::rect<s32>(core::position2d<s32>(0, 0),
core::dimension2di(texture->getOriginalSize())), core::dimension2di(texture->getOriginalSize())),
NULL/*&AbsoluteClippingRect*/, colors, true); NULL/*&AbsoluteClippingRect*/, colors, true);
} else {
// `-x` is interpreted as `w - x`
if (middle.LowerRightCorner.X < 0) {
middle.LowerRightCorner.X += texture->getOriginalSize().Width;
}
if (middle.LowerRightCorner.Y < 0) {
middle.LowerRightCorner.Y += texture->getOriginalSize().Height;
}
draw2DImage9Slice(driver, texture, rect, middle);
}
} else { } else {
errorstream << "GUIFormSpecMenu::drawMenu() Draw backgrounds unable to load texture:" << std::endl; errorstream << "GUIFormSpecMenu::drawMenu() Draw backgrounds unable to load texture:" << std::endl;
errorstream << "\t" << spec.name << std::endl; errorstream << "\t" << spec.name << std::endl;

@ -176,6 +176,18 @@ class GUIFormSpecMenu : public GUIModalMenu
{ {
} }
ImageDrawSpec(const std::string &a_name,
const v2s32 &a_pos, const v2s32 &a_geom, const core::rect<s32> &middle, bool clip=false):
name(a_name),
parent_button(NULL),
pos(a_pos),
geom(a_geom),
middle(middle),
scale(true),
clip(clip)
{
}
ImageDrawSpec(const std::string &a_name, ImageDrawSpec(const std::string &a_name,
const v2s32 &a_pos): const v2s32 &a_pos):
name(a_name), name(a_name),
@ -191,6 +203,7 @@ class GUIFormSpecMenu : public GUIModalMenu
gui::IGUIButton *parent_button; gui::IGUIButton *parent_button;
v2s32 pos; v2s32 pos;
v2s32 geom; v2s32 geom;
core::rect<s32> middle;
bool scale; bool scale;
bool clip; bool clip;
}; };