mirror of https://github.com/minetest/minetest.git
453 lines
13 KiB
C++
453 lines
13 KiB
C++
/*
|
|
Minetest
|
|
Copyright (C) 2023 v-rob, Vincent Robinson <robinsonvincent89@gmail.com>
|
|
|
|
This program is free software; you can redistribute it and/or modify
|
|
it under the terms of the GNU Lesser General Public License as published by
|
|
the Free Software Foundation; either version 2.1 of the License, or
|
|
(at your option) any later version.
|
|
|
|
This program is distributed in the hope that it will be useful,
|
|
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
GNU Lesser General Public License for more details.
|
|
|
|
You should have received a copy of the GNU Lesser General Public License along
|
|
with this program; if not, write to the Free Software Foundation, Inc.,
|
|
51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
|
|
*/
|
|
|
|
#include "gui/box.h"
|
|
|
|
#include "debug.h"
|
|
#include "log.h"
|
|
#include "porting.h"
|
|
#include "gui/elem.h"
|
|
#include "gui/manager.h"
|
|
#include "gui/window.h"
|
|
#include "util/serialize.h"
|
|
|
|
namespace ui
|
|
{
|
|
Align toAlign(u8 align)
|
|
{
|
|
if (align >= (u8)Align::MAX_ALIGN) {
|
|
return Align::CENTER;
|
|
}
|
|
return (Align)align;
|
|
}
|
|
|
|
void Layer::reset()
|
|
{
|
|
image = Texture();
|
|
fill = BLANK;
|
|
tint = WHITE;
|
|
|
|
source = rf32(0.0f, 0.0f, 1.0f, 1.0f);
|
|
middle = rf32(0.0f, 0.0f, 0.0f, 0.0f);
|
|
middle_scale = 1.0f;
|
|
|
|
num_frames = 1;
|
|
frame_time = 1000;
|
|
}
|
|
|
|
void Layer::read(std::istream &full_is)
|
|
{
|
|
auto is = newIs(readStr16(full_is));
|
|
u32 set_mask = readU32(is);
|
|
|
|
if (testShift(set_mask))
|
|
image = g_manager.getTexture(readNullStr(is));
|
|
if (testShift(set_mask))
|
|
fill = readARGB8(is);
|
|
if (testShift(set_mask))
|
|
tint = readARGB8(is);
|
|
|
|
if (testShift(set_mask)) {
|
|
source.UpperLeftCorner = readV2F32(is);
|
|
source.LowerRightCorner = readV2F32(is);
|
|
}
|
|
if (testShift(set_mask)) {
|
|
middle.UpperLeftCorner = clamp_vec(readV2F32(is));
|
|
middle.LowerRightCorner = clamp_vec(readV2F32(is));
|
|
}
|
|
if (testShift(set_mask))
|
|
middle_scale = std::max(readF32(is), 0.0f);
|
|
|
|
if (testShift(set_mask))
|
|
num_frames = std::max(readU32(is), 1U);
|
|
if (testShift(set_mask))
|
|
frame_time = std::max(readU32(is), 1U);
|
|
}
|
|
|
|
void Style::reset()
|
|
{
|
|
size = d2f32(0.0f, 0.0f);
|
|
|
|
rel_pos = v2f32(0.0f, 0.0f);
|
|
rel_anchor = v2f32(0.0f, 0.0f);
|
|
rel_size = d2s32(1.0f, 1.0f);
|
|
|
|
margin = rf32(0.0f, 0.0f, 0.0f, 0.0f);
|
|
padding = rf32(0.0f, 0.0f, 0.0f, 0.0f);
|
|
|
|
bg.reset();
|
|
fg.reset();
|
|
|
|
fg_scale = 1.0f;
|
|
fg_halign = Align::CENTER;
|
|
fg_valign = Align::CENTER;
|
|
|
|
visible = true;
|
|
noclip = false;
|
|
}
|
|
|
|
void Style::read(std::istream &is)
|
|
{
|
|
// No need to read a size prefix; styles are already read in as size-
|
|
// prefixed strings in Window.
|
|
u32 set_mask = readU32(is);
|
|
|
|
if (testShift(set_mask))
|
|
size = clamp_vec(readV2F32(is));
|
|
|
|
if (testShift(set_mask))
|
|
rel_pos = readV2F32(is);
|
|
if (testShift(set_mask))
|
|
rel_anchor = readV2F32(is);
|
|
if (testShift(set_mask))
|
|
rel_size = clamp_vec(readV2F32(is));
|
|
|
|
if (testShift(set_mask)) {
|
|
margin.UpperLeftCorner = readV2F32(is);
|
|
margin.LowerRightCorner = readV2F32(is);
|
|
}
|
|
if (testShift(set_mask)) {
|
|
padding.UpperLeftCorner = readV2F32(is);
|
|
padding.LowerRightCorner = readV2F32(is);
|
|
}
|
|
|
|
if (testShift(set_mask))
|
|
bg.read(is);
|
|
if (testShift(set_mask))
|
|
fg.read(is);
|
|
|
|
if (testShift(set_mask))
|
|
fg_scale = std::max(readF32(is), 0.0f);
|
|
if (testShift(set_mask))
|
|
fg_halign = toAlign(readU8(is));
|
|
if (testShift(set_mask))
|
|
fg_valign = toAlign(readU8(is));
|
|
|
|
if (testShift(set_mask))
|
|
visible = testShift(set_mask);
|
|
if (testShift(set_mask))
|
|
noclip = testShift(set_mask);
|
|
}
|
|
|
|
Window &Box::getWindow()
|
|
{
|
|
return m_elem.getWindow();
|
|
}
|
|
|
|
const Window &Box::getWindow() const
|
|
{
|
|
return m_elem.getWindow();
|
|
}
|
|
|
|
void Box::reset()
|
|
{
|
|
m_style.reset();
|
|
|
|
for (State i = 0; i < m_style_refs.size(); i++) {
|
|
m_style_refs[i] = NO_STYLE;
|
|
}
|
|
|
|
m_draw_rect = rf32(0.0f, 0.0f, 0.0f, 0.0f);
|
|
m_child_rect = m_draw_rect;
|
|
m_clip_rect = m_draw_rect;
|
|
}
|
|
|
|
void Box::read(std::istream &full_is)
|
|
{
|
|
auto is = newIs(readStr16(full_is));
|
|
u32 style_mask = readU32(is);
|
|
|
|
for (State i = 0; i < m_style_refs.size(); i++) {
|
|
// If we have a style for this state in the mask, add it to the
|
|
// list of styles.
|
|
if (!testShift(style_mask)) {
|
|
continue;
|
|
}
|
|
|
|
u32 style = readU32(is);
|
|
if (getWindow().getStyleStr(style) != nullptr) {
|
|
m_style_refs[i] = style;
|
|
} else {
|
|
errorstream << "Style " << style << " does not exist" << std::endl;
|
|
}
|
|
}
|
|
}
|
|
|
|
void Box::layout(const rf32 &parent_rect, const rf32 &parent_clip)
|
|
{
|
|
// Before we layout the box, we need to recompute the style so we have
|
|
// fully updated style properties.
|
|
computeStyle();
|
|
|
|
// First, calculate the size of the box in absolute coordinates based
|
|
// on the normalized size.
|
|
d2f32 origin_size(
|
|
(m_style.rel_size.Width * parent_rect.getWidth()),
|
|
(m_style.rel_size.Height * parent_rect.getHeight())
|
|
);
|
|
|
|
// Ensure that the normalized size of the box isn't smaller than the
|
|
// minimum size.
|
|
origin_size.Width = std::max(origin_size.Width, m_style.size.Width);
|
|
origin_size.Height = std::max(origin_size.Height, m_style.size.Height);
|
|
|
|
// Then, create the rect of the box relative to the origin by
|
|
// converting the normalized position absolute coordinates, while
|
|
// accounting for the anchor based on the previously calculated size.
|
|
v2f32 origin_pos(
|
|
(m_style.rel_pos.X * parent_rect.getWidth()) -
|
|
(m_style.rel_anchor.X * origin_size.Width),
|
|
(m_style.rel_pos.Y * parent_rect.getHeight()) -
|
|
(m_style.rel_anchor.Y * origin_size.Height)
|
|
);
|
|
|
|
rf32 origin_rect(origin_pos, origin_size);
|
|
|
|
// The absolute rect of the box is made by shifting the origin to the
|
|
// top left of the parent rect.
|
|
rf32 abs_rect = origin_rect + parent_rect.UpperLeftCorner;
|
|
|
|
// The rect we draw to is the absolute rect adjusted for the margins.
|
|
// Since this is the final rect, we ensure that it doesn't have a
|
|
// negative size.
|
|
m_draw_rect = clamp_rect(rf32(
|
|
abs_rect.UpperLeftCorner + m_style.margin.UpperLeftCorner,
|
|
abs_rect.LowerRightCorner - m_style.margin.LowerRightCorner
|
|
));
|
|
|
|
// The rect that children and the foreground layer are drawn relative
|
|
// to is the draw rect adjusted for padding. Make sure this rect is
|
|
// never negative as well.
|
|
m_child_rect = clamp_rect(rf32(
|
|
m_draw_rect.UpperLeftCorner + m_style.padding.UpperLeftCorner,
|
|
m_draw_rect.LowerRightCorner - m_style.padding.LowerRightCorner
|
|
));
|
|
|
|
// If we are set to noclip, we clip to the same rect we draw to.
|
|
// Otherwise, the clip rect is the drawing rect clipped against the
|
|
// parent clip rect.
|
|
m_clip_rect = m_style.noclip ? m_draw_rect : clip_rect(m_draw_rect, parent_clip);
|
|
}
|
|
|
|
void Box::draw(Canvas &parent)
|
|
{
|
|
// Since layout() is always called before draw(), we already have fully
|
|
// updated style properties.
|
|
|
|
// Don't draw anything if we aren't visible.
|
|
if (!m_style.visible) {
|
|
return;
|
|
}
|
|
|
|
// Create a new canvas relative to our parent to draw to.
|
|
Canvas canvas(parent, m_style.noclip ? nullptr : &m_clip_rect);
|
|
|
|
// Draw our background and foreground layers.
|
|
drawLayer(canvas, m_style.bg, m_draw_rect);
|
|
drawForeground(canvas);
|
|
}
|
|
|
|
void Box::drawForeground(Canvas &canvas)
|
|
{
|
|
// It makes no sense to draw a foreground when there's no image, since
|
|
// it would otherwise take no room.
|
|
if (!m_style.fg.image.isTexture()) {
|
|
return;
|
|
}
|
|
|
|
// The foreground layer is aligned and scaled in a particular area of
|
|
// the box. First, get the size of the foreground layer.
|
|
d2f32 src_size = m_style.fg.source.getSize();
|
|
src_size.Height /= m_style.fg.num_frames;
|
|
|
|
d2s32 tex_size = m_style.fg.image.getSize();
|
|
src_size.Width *= tex_size.Width;
|
|
src_size.Height *= tex_size.Height;
|
|
|
|
// Then, compute the scale that we should use. A scale of zero means
|
|
// the image should take up as much room as possible while still
|
|
// preserving the aspect ratio of the image.
|
|
float scale = m_style.fg_scale;
|
|
|
|
if (scale == 0.0f) {
|
|
scale = std::min(
|
|
m_child_rect.getWidth() / src_size.Width,
|
|
m_child_rect.getHeight() / src_size.Height
|
|
);
|
|
}
|
|
|
|
d2f32 fg_size(src_size.Width * scale, src_size.Height * scale);
|
|
|
|
// Now, using the alignment options, position the foreground image
|
|
// inside the remaining space.
|
|
v2f32 fg_pos = m_child_rect.UpperLeftCorner;
|
|
|
|
if (m_style.fg_halign == Align::CENTER) {
|
|
fg_pos.X += (m_child_rect.getWidth() - fg_size.Width) / 2.0f;
|
|
} else if (m_style.fg_halign == Align::END) {
|
|
fg_pos.X += m_child_rect.getWidth() - fg_size.Width;
|
|
}
|
|
|
|
if (m_style.fg_valign == Align::CENTER) {
|
|
fg_pos.Y += (m_child_rect.getHeight() - fg_size.Height) / 2.0f;
|
|
} else if (m_style.fg_valign == Align::END) {
|
|
fg_pos.Y += m_child_rect.getHeight() - fg_size.Height;
|
|
}
|
|
|
|
// We have our position and size, so now we can draw the layer.
|
|
drawLayer(canvas, m_style.fg, rf32(fg_pos, fg_size));
|
|
}
|
|
|
|
void Box::drawLayer(Canvas &canvas, const Layer &layer, const rf32 &dst)
|
|
{
|
|
// Draw the fill color if it's not totally transparent.
|
|
if (layer.fill.getAlpha() != 0x0) {
|
|
canvas.drawRect(dst, layer.fill);
|
|
}
|
|
|
|
// If there's no image, there's nothing else for us to do.
|
|
if (!layer.image.isTexture()) {
|
|
return;
|
|
}
|
|
|
|
// If we have animations, we need to adjust the source rect by the
|
|
// frame offset in accordance with the current frame.
|
|
rf32 src = layer.source;
|
|
|
|
if (layer.num_frames > 1) {
|
|
float frame_height = src.getHeight() / layer.num_frames;
|
|
src.LowerRightCorner.Y = src.UpperLeftCorner.Y + frame_height;
|
|
|
|
float frame_offset = frame_height *
|
|
((porting::getTimeMs() / layer.frame_time) % layer.num_frames);
|
|
src.UpperLeftCorner.Y += frame_offset;
|
|
src.LowerRightCorner.Y += frame_offset;
|
|
}
|
|
|
|
// If the source rect for this image is flipped, we need to flip the
|
|
// sign of our middle rect as well to get the right adjustments.
|
|
rf32 src_middle = layer.middle;
|
|
|
|
if (src.getWidth() < 0.0f) {
|
|
src_middle.UpperLeftCorner.X = -src_middle.UpperLeftCorner.X;
|
|
src_middle.LowerRightCorner.X = -src_middle.LowerRightCorner.X;
|
|
}
|
|
if (src.getHeight() < 0.0f) {
|
|
src_middle.UpperLeftCorner.Y = -src_middle.UpperLeftCorner.Y;
|
|
src_middle.LowerRightCorner.Y = -src_middle.LowerRightCorner.Y;
|
|
}
|
|
|
|
// Now we need to draw the texture as a nine-slice image. But first,
|
|
// since the middle rect uses normalized coordinates, we need to
|
|
// de-normalize it into actual pixels for the destination rect and
|
|
// scale it by the middle rect scaling parameter.
|
|
rf32 scaled_middle(
|
|
layer.middle.UpperLeftCorner.X * layer.middle_scale * layer.image.getWidth(),
|
|
layer.middle.UpperLeftCorner.Y * layer.middle_scale * layer.image.getHeight(),
|
|
layer.middle.LowerRightCorner.X * layer.middle_scale * layer.image.getWidth(),
|
|
layer.middle.LowerRightCorner.Y * layer.middle_scale * layer.image.getHeight()
|
|
);
|
|
|
|
// Now draw each slice of the nine-slice image. If the middle rect
|
|
// equals the whole source rect, this will automatically act like a
|
|
// normal image.
|
|
for (int y = 0; y < 3; y++) {
|
|
for (int x = 0; x < 3; x++) {
|
|
rf32 slice_src = src;
|
|
rf32 slice_dst = dst;
|
|
|
|
switch (x) {
|
|
case 0:
|
|
slice_dst.LowerRightCorner.X =
|
|
dst.UpperLeftCorner.X + scaled_middle.UpperLeftCorner.X;
|
|
slice_src.LowerRightCorner.X =
|
|
src.UpperLeftCorner.X + src_middle.UpperLeftCorner.X;
|
|
break;
|
|
|
|
case 1:
|
|
slice_dst.UpperLeftCorner.X += scaled_middle.UpperLeftCorner.X;
|
|
slice_dst.LowerRightCorner.X -= scaled_middle.LowerRightCorner.X;
|
|
slice_src.UpperLeftCorner.X += src_middle.UpperLeftCorner.X;
|
|
slice_src.LowerRightCorner.X -= src_middle.LowerRightCorner.X;
|
|
break;
|
|
|
|
case 2:
|
|
slice_dst.UpperLeftCorner.X =
|
|
dst.LowerRightCorner.X - scaled_middle.LowerRightCorner.X;
|
|
slice_src.UpperLeftCorner.X =
|
|
src.LowerRightCorner.X - src_middle.LowerRightCorner.X;
|
|
break;
|
|
}
|
|
|
|
switch (y) {
|
|
case 0:
|
|
slice_dst.LowerRightCorner.Y =
|
|
dst.UpperLeftCorner.Y + scaled_middle.UpperLeftCorner.Y;
|
|
slice_src.LowerRightCorner.Y =
|
|
src.UpperLeftCorner.Y + src_middle.UpperLeftCorner.Y;
|
|
break;
|
|
|
|
case 1:
|
|
slice_dst.UpperLeftCorner.Y += scaled_middle.UpperLeftCorner.Y;
|
|
slice_dst.LowerRightCorner.Y -= scaled_middle.LowerRightCorner.Y;
|
|
slice_src.UpperLeftCorner.Y += src_middle.UpperLeftCorner.Y;
|
|
slice_src.LowerRightCorner.Y -= src_middle.LowerRightCorner.Y;
|
|
break;
|
|
|
|
case 2:
|
|
slice_dst.UpperLeftCorner.Y =
|
|
dst.LowerRightCorner.Y - scaled_middle.LowerRightCorner.Y;
|
|
slice_src.UpperLeftCorner.Y =
|
|
src.LowerRightCorner.Y - src_middle.LowerRightCorner.Y;
|
|
break;
|
|
}
|
|
|
|
// Draw this slice of the texture with the proper tint.
|
|
canvas.drawTexture(slice_dst, layer.image, slice_src, layer.tint);
|
|
}
|
|
}
|
|
}
|
|
|
|
void Box::computeStyle()
|
|
{
|
|
// First, clear our current style and compute what state we're in.
|
|
m_style.reset();
|
|
State state = STATE_NONE;
|
|
|
|
// Loop over each style state from lowest precedence to highest since
|
|
// they should be applied in that order.
|
|
for (State i = 0; i < m_style_refs.size(); i++) {
|
|
// If this state we're looking at is a subset of the current state,
|
|
// then it's a match for styling.
|
|
if ((state & i) != i) {
|
|
continue;
|
|
}
|
|
|
|
u32 index = m_style_refs[i];
|
|
|
|
// If the index for this state has an associated style string,
|
|
// apply it to our current style.
|
|
if (index != NO_STYLE) {
|
|
auto is = newIs(*getWindow().getStyleStr(index));
|
|
m_style.read(is);
|
|
}
|
|
}
|
|
}
|
|
}
|