1
0
Fork 0
mirror of https://github.com/luanti-org/luanti.git synced 2025-08-01 17:38:41 +00:00
luanti/src/ui/box.cpp
2025-06-01 18:41:24 -07:00

666 lines
19 KiB
C++

// Luanti
// SPDX-License-Identifier: LGPL-2.1-or-later
// Copyright (C) 2023 v-rob, Vincent Robinson <robinsonvincent89@gmail.com>
#include "ui/box.h"
#include "debug.h"
#include "log.h"
#include "porting.h"
#include "ui/elem.h"
#include "ui/manager.h"
#include "ui/window.h"
#include "util/serialize.h"
#include <SDL2/SDL.h>
namespace ui
{
Window &Box::getWindow()
{
return m_elem.getWindow();
}
const Window &Box::getWindow() const
{
return m_elem.getWindow();
}
void Box::reset()
{
m_content.clear();
m_style.reset();
for (State i = 0; i < m_style_refs.size(); i++) {
m_style_refs[i] = NO_STYLE;
}
}
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 index = readU32(is);
if (getWindow().getStyleStr(index) != nullptr) {
m_style_refs[i] = index;
} else {
errorstream << "Style " << index << " does not exist" << std::endl;
}
}
}
void Box::restyle()
{
// First, clear our current style and compute what state we're in.
m_style.reset();
State state = STATE_NONE;
if (m_elem.isBoxFocused(*this))
state |= STATE_FOCUSED;
if (m_elem.isBoxSelected(*this))
state |= STATE_SELECTED;
if (m_elem.isBoxHovered(*this))
state |= STATE_HOVERED;
if (m_elem.isBoxPressed(*this))
state |= STATE_PRESSED;
if (m_elem.isBoxDisabled(*this))
state |= STATE_DISABLED;
// 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);
}
}
// Since our box has been restyled, the previously computed layout
// information is no longer valid.
m_min_layout = SizeF();
m_min_content = SizeF();
m_display_rect = RectF();
m_icon_rect = RectF();
m_content_rect = RectF();
m_clip_rect = RectF();
// Finally, make sure to restyle our content.
for (Box *box : m_content) {
box->restyle();
}
}
void Box::resize()
{
for (Box *box : m_content) {
box->resize();
}
switch (m_style.layout.type) {
case LayoutType::PLACE:
resizePlace();
break;
}
resizeBox();
}
void Box::relayout(RectF layout_rect, RectF layout_clip)
{
relayoutBox(layout_rect, layout_clip);
switch (m_style.layout.type) {
case LayoutType::PLACE:
relayoutPlace();
break;
}
}
void Box::draw()
{
if (m_style.display != DisplayMode::HIDDEN) {
drawBox();
drawIcon();
}
for (Box *box : m_content) {
box->draw();
}
}
bool Box::isPointed() const
{
return m_clip_rect.contains(getWindow().getPointerPos());
}
bool Box::isContentPointed() const {
// If we're pointed, then we clearly have a pointed box.
if (isPointed()) {
return true;
}
// Search through our content. If any of them are contained within the
// same element as this box, they are candidates for being pointed.
for (Box *box : m_content) {
if (&box->getElem() == &m_elem && box->isContentPointed()) {
return true;
}
}
return false;
}
bool Box::processInput(const SDL_Event &event)
{
switch (event.type) {
case UI_USER(FOCUS_REQUEST):
// The box is dynamic, so it can be focused.
return true;
case UI_USER(FOCUS_CHANGED):
// If the box is no longer focused, it can't be pressed.
if (event.user.data1 == &m_elem) {
setPressed(false);
}
return false;
case UI_USER(FOCUS_SUBVERTED):
// If some non-focused element used an event instead of this one,
// unpress the box because user interaction has been diverted.
setPressed(false);
return false;
case UI_USER(HOVER_REQUEST):
// The box can be hovered if the pointer is inside it.
return isPointed();
case UI_USER(HOVER_CHANGED):
// Make this box hovered if the element became hovered and the
// pointer is inside this box.
setHovered(event.user.data2 == &m_elem && isPointed());
return true;
default:
return false;
}
}
bool Box::processFullPress(const SDL_Event &event, void (*on_press)(Elem &))
{
switch (event.type) {
case SDL_KEYDOWN:
// If the space key is pressed not due to a key repeat, then the
// box becomes pressed. If the escape key is pressed while the box
// is pressed, that unpresses the box without triggering it.
if (event.key.keysym.sym == SDLK_SPACE && !event.key.repeat) {
setPressed(true);
return true;
} else if (event.key.keysym.sym == SDLK_ESCAPE && isPressed()) {
setPressed(false);
return true;
}
return false;
case SDL_KEYUP:
// Releasing the space key while the box is pressed causes it to be
// unpressed and triggered.
if (event.key.keysym.sym == SDLK_SPACE && isPressed()) {
setPressed(false);
on_press(m_elem);
return true;
}
return false;
case SDL_MOUSEBUTTONDOWN:
// If the box is hovered, then pressing the left mouse button
// causes it to be pressed. Otherwise, the mouse is directed at
// some other box.
if (isHovered() && event.button.button == SDL_BUTTON_LEFT) {
setPressed(true);
return true;
}
return false;
case SDL_MOUSEBUTTONUP:
// If the mouse button was released, the box becomes unpressed. If
// it was released while inside the bounds of the box, that counts
// as the box being triggered.
if (event.button.button == SDL_BUTTON_LEFT) {
bool was_pressed = isPressed();
setPressed(false);
if (isHovered() && was_pressed) {
on_press(m_elem);
return true;
}
}
return false;
default:
return processInput(event);
}
}
RectF Box::getLayerSource(const Layer &layer)
{
RectF src = layer.source;
// If we have animations, we need to adjust the source rect by the
// frame offset in accordance with the current frame.
if (layer.num_frames > 1) {
float frame_height = src.H() / layer.num_frames;
src.B = src.T + frame_height;
float frame_offset = frame_height *
((porting::getTimeMs() / layer.frame_time) % layer.num_frames);
src.T += frame_offset;
src.B += frame_offset;
}
return src;
}
SizeF Box::getLayerSize(const Layer &layer)
{
return getLayerSource(layer).size() * getTextureSize(layer.image);
}
DispF Box::getMiddleEdges()
{
// Scale the middle rect by the scaling factor and de-normalize it into
// actual pixels based on the image source rect.
return m_style.box_middle * DispF(getLayerSize(m_style.box)) * m_style.box.scale;
}
void Box::resizeBox()
{
// If the box is set to clip its contents in either dimension, we can
// set the minimum content size to zero for that coordinate.
if (m_style.layout.clip == DirFlags::X || m_style.layout.clip == DirFlags::BOTH) {
m_min_content.W = 0.0f;
}
if (m_style.layout.clip == DirFlags::Y || m_style.layout.clip == DirFlags::BOTH) {
m_min_content.H = 0.0f;
}
// We need to factor the icon into the minimum size of the box. The
// minimum size of the padding rect is either the size of the contents
// or the scaled icon, depending on which is larger. If the scale is
// zero, then the icon doesn't add anything to the minimum size.
SizeF icon_size = getLayerSize(m_style.icon) * m_style.icon.scale;
SizeF padding_size = m_min_content.max(icon_size);
// If the icon should not overlap the content, then we must take into
// account the extra space required for this, including the gutter.
if (!m_style.icon_overlap && m_style.icon.image != nullptr) {
switch (m_style.icon_place) {
case IconPlace::CENTER:
break;
case IconPlace::LEFT:
case IconPlace::RIGHT:
padding_size.W = m_min_content.W + icon_size.W + m_style.icon_gutter;
break;
case IconPlace::TOP:
case IconPlace::BOTTOM:
padding_size.H = m_min_content.H + icon_size.H + m_style.icon_gutter;
break;
}
padding_size = padding_size.clip();
}
// Now that we have a minimum size for the padding rect, we can
// calculate the display rect size by adjusting for the padding and
// middle rect edges. We also clamp the size of the display rect to be
// at least as large as the user-specified minimum size.
SizeF display_size = (padding_size + getMiddleEdges().extents() +
m_style.sizing.padding.extents()).max(m_style.sizing.size);
// The final minimum size is the display size adjusted for the margin.
m_min_layout = (display_size + m_style.sizing.margin.extents()).clip();
}
void Box::relayoutBox(RectF layout_rect, RectF layout_clip)
{
// The display rect is created by insetting the layout rect by the
// margin. The padding rect is inset from that by the middle rect edges
// and the padding. We must make sure these do not have negative sizes.
m_display_rect = layout_rect.insetBy(m_style.sizing.margin).clip();
RectF padding_rect = m_display_rect.insetBy(
getMiddleEdges() + m_style.sizing.padding).clip();
// The icon is aligned and scaled in a particular area of the box.
// First, get the basic size of the icon rect.
SizeF icon_size = getLayerSize(m_style.icon);
// Then, modify it based on 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.
if (m_style.icon.scale == 0.0f) {
SizeF max_icon = padding_rect.size();
// If the icon should not overlap the content, then we need to
// adjust the area in which we compute the maximum scale by
// subtracting the content and gutter from the padding rect size.
if (!m_style.icon_overlap && m_style.icon.image != nullptr) {
switch (m_style.icon_place) {
case IconPlace::CENTER:
break;
case IconPlace::LEFT:
case IconPlace::RIGHT:
max_icon.W -= m_min_content.W + m_style.icon_gutter;
break;
case IconPlace::TOP:
case IconPlace::BOTTOM:
max_icon.H -= m_min_content.H + m_style.icon_gutter;
break;
}
max_icon = max_icon.clip();
}
// Choose the scale factor based on the space we have for the icon,
// choosing the smaller of the two possible image size ratios.
icon_size *= std::min(max_icon.W / icon_size.W, max_icon.H / icon_size.H);
} else {
icon_size *= m_style.icon.scale;
}
// Now that we have the size of the icon, we can compute the icon rect
// based on the desired placement of the icon.
PosF icon_start = padding_rect.TopLeft;
PosF icon_center = icon_start + (padding_rect.size() - icon_size) / 2.0f;
PosF icon_end = icon_start + (padding_rect.size() - icon_size);
switch (m_style.icon_place) {
case IconPlace::CENTER:
m_icon_rect = RectF(icon_center, icon_size);
break;
case IconPlace::LEFT:
m_icon_rect = RectF(PosF(icon_start.X, icon_center.Y), icon_size);
break;
case IconPlace::TOP:
m_icon_rect = RectF(PosF(icon_center.X, icon_start.Y), icon_size);
break;
case IconPlace::RIGHT:
m_icon_rect = RectF(PosF(icon_end.X, icon_center.Y), icon_size);
break;
case IconPlace::BOTTOM:
m_icon_rect = RectF(PosF(icon_center.X, icon_end.Y), icon_size);
break;
}
// If the overlap property is set or the icon is centered, the content
// rect is identical to the padding rect. Otherwise, the content rect
// needs to be adjusted to account for the icon and gutter.
m_content_rect = padding_rect;
if (!m_style.icon_overlap && m_style.icon.image != nullptr) {
switch (m_style.icon_place) {
case IconPlace::CENTER:
break;
case IconPlace::LEFT:
m_content_rect.L += icon_size.W + m_style.icon_gutter;
break;
case IconPlace::TOP:
m_content_rect.T += icon_size.H + m_style.icon_gutter;
break;
case IconPlace::RIGHT:
m_content_rect.R -= icon_size.W + m_style.icon_gutter;
break;
case IconPlace::BOTTOM:
m_content_rect.B -= icon_size.H + m_style.icon_gutter;
break;
}
m_content_rect = m_content_rect.clip();
}
// We set our clipping rect based on the display mode.
switch (m_style.display) {
case DisplayMode::VISIBLE:
case DisplayMode::HIDDEN:
// If the box is visible or hidden, then we clip the box and its
// content as normal against the drawing and layout clip rects.
m_clip_rect = m_display_rect.intersectWith(layout_clip);
break;
case DisplayMode::OVERFLOW:
// If the box allows overflow, then clip to the drawing rect, since
// we never want to expand outside our own visible boundaries, but
// we don't clip to the layout clip rect.
m_clip_rect = m_display_rect;
break;
case DisplayMode::CLIPPED:
// If the box and its content should be entirely removed, then we
// clip everything entirely.
m_clip_rect = RectF();
break;
}
}
void Box::resizePlace()
{
for (Box *box : m_content) {
// Calculate the size of the box according to the span and scale
// factor. If the scale is zero, we don't know how big the span
// will end up being, so the span size goes to zero.
SizeF span_size = box->m_style.sizing.span * m_style.layout.scale;
// Ensure that the computed minimum size for our content is at
// least as large as the minimum size of the box and its span size.
m_min_content = m_min_content.max(box->m_min_layout).max(span_size);
}
}
void Box::relayoutPlace()
{
for (Box *box : m_content) {
const Sizing &sizing = box->m_style.sizing;
// Compute the scale factor. If the scale is zero, then we use the
// size of the parent box to achieve normalized coordinates.
SizeF scale = m_style.layout.scale == 0.0f ?
m_content_rect.size() : SizeF(m_style.layout.scale);
// Calculate the position and size of the box relative to the
// origin, taking into account the scale factor and anchor. Also
// make sure the size doesn't go below the minimum size.
SizeF size = (sizing.span * scale).max(box->m_min_layout);
SizeF pos = (sizing.pos * scale) - (sizing.anchor * size);
// The layout rect of the box is made by shifting the above rect by
// the top left of the content rect.
RectF layout_rect = RectF(m_content_rect.TopLeft + pos, size);
box->relayout(layout_rect, m_clip_rect);
}
}
void Box::drawBox()
{
// First, fill the display rectangle with the fill color.
getWindow().drawRect(m_display_rect, m_clip_rect, m_style.box.fill);
// If there's no image, then we don't need to do a bunch of
// calculations in order to draw nothing.
if (m_style.box.image == nullptr) {
return;
}
// For the image, first get the source rect adjusted for animations.
RectF src = getLayerSource(m_style.box);
// We need to make sure the the middle rect is relative to the source
// rect rather than the entire image, so scale the edges appropriately.
DispF middle_src = m_style.box_middle * DispF(src.size());
DispF middle_dst = getMiddleEdges();
// 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.
if (src.W() < 0.0f) {
middle_src.L = -middle_src.L;
middle_src.R = -middle_src.R;
}
if (src.H() < 0.0f) {
middle_src.T = -middle_src.T;
middle_src.B = -middle_src.B;
}
for (int slice_y = 0; slice_y < 3; slice_y++) {
for (int slice_x = 0; slice_x < 3; slice_x++) {
// Compute each slice of the nine-slice image. If the middle
// rect equals the whole source rect, the middle slice will
// occupy the entire display rectangle.
RectF slice_src = src;
RectF slice_dst = m_display_rect;
switch (slice_x) {
case 0:
slice_dst.R = slice_dst.L + middle_dst.L;
slice_src.R = slice_src.L + middle_src.L;
break;
case 1:
slice_dst.L += middle_dst.L;
slice_dst.R -= middle_dst.R;
slice_src.L += middle_src.L;
slice_src.R -= middle_src.R;
break;
case 2:
slice_dst.L = slice_dst.R - middle_dst.R;
slice_src.L = slice_src.R - middle_src.R;
break;
}
switch (slice_y) {
case 0:
slice_dst.B = slice_dst.T + middle_dst.T;
slice_src.B = slice_src.T + middle_src.T;
break;
case 1:
slice_dst.T += middle_dst.T;
slice_dst.B -= middle_dst.B;
slice_src.T += middle_src.T;
slice_src.B -= middle_src.B;
break;
case 2:
slice_dst.T = slice_dst.B - middle_dst.B;
slice_src.T = slice_src.B - middle_src.B;
break;
}
// If we have a tiled image, then some of the tiles may bleed
// out of the slice rect, so we need to clip to both the
// clipping rect and the destination rect.
RectF slice_clip = m_clip_rect.intersectWith(slice_dst);
// If this slice is empty or has been entirely clipped, then
// don't bother drawing anything.
if (slice_clip.empty()) {
continue;
}
// This may be a tiled image, so we need to calculate the size
// of each tile. If the image is not tiled, this should equal
// the size of the destination rect.
SizeF tile_size = slice_dst.size();
if (m_style.box_tile != DirFlags::NONE) {
// We need to calculate the tile size based on the texture
// size and the scale of each tile. If the scale is too
// small, then the number of tiles will explode, so we
// clamp it to a reasonable minimum of 1/8 of a pixel.
SizeF tex_size = getTextureSize(m_style.box.image);
float tile_scale = std::max(m_style.box.scale, 0.125f);
if (m_style.box_tile != DirFlags::Y) {
tile_size.W = slice_src.W() * tex_size.W * tile_scale;
}
if (m_style.box_tile != DirFlags::X) {
tile_size.H = slice_src.H() * tex_size.H * tile_scale;
}
}
// Now we can draw each tile for this slice. If the image is
// not tiled, then each of these loops will run only once.
float tile_y = slice_dst.T;
while (tile_y < slice_dst.B) {
float tile_x = slice_dst.L;
while (tile_x < slice_dst.R) {
// Draw the texture in the appropriate destination rect
// for this tile, and clip it to the clipping rect for
// this slice.
RectF tile_dst = RectF(PosF(tile_x, tile_y), tile_size);
getWindow().drawTexture(tile_dst, slice_clip,
m_style.box.image, slice_src, m_style.box.tint);
tile_x += tile_size.W;
}
tile_y += tile_size.H;
}
}
}
}
void Box::drawIcon()
{
// The icon rect is computed while the box is being laid out, so we
// just need to draw it with the fill color behind it.
getWindow().drawRect(m_icon_rect, m_clip_rect, m_style.icon.fill);
getWindow().drawTexture(m_icon_rect, m_clip_rect, m_style.icon.image,
getLayerSource(m_style.icon), m_style.icon.tint);
}
bool Box::isHovered() const
{
return m_elem.getHoveredBox() == getId();
}
bool Box::isPressed() const
{
return m_elem.getPressedBox() == getId();
}
void Box::setHovered(bool hovered)
{
if (hovered) {
m_elem.setHoveredBox(getId());
} else if (isHovered()) {
m_elem.setHoveredBox(NO_ID);
}
}
void Box::setPressed(bool pressed)
{
if (pressed) {
m_elem.setPressedBox(getId());
} else if (isPressed()) {
m_elem.setPressedBox(NO_ID);
}
}
}