1
0
Fork 0
mirror of https://github.com/luanti-org/luanti.git synced 2025-07-22 17:18:39 +00:00

Add preliminary text support

This commit is contained in:
v-rob 2025-05-27 13:06:59 -07:00
parent bb2f857b04
commit c7fca1b956
11 changed files with 323 additions and 3 deletions

View file

@ -45,6 +45,8 @@ function ui.Elem:new(param)
end
function ui.Elem:_init(props)
self._label = ui._opt(props.label, "string")
self._groups = {}
self._children = {}
@ -114,6 +116,10 @@ function ui.Elem:_encode_fields()
ui._encode_flag(fl, "Z", ui._encode_array("z", child_ids))
end
if ui._shift_flag(fl, self._label) then
ui._encode_flag(fl, "s", self._label)
end
self:_encode_box(fl, self._boxes.main)
return ui._encode_flags(fl)

View file

@ -82,6 +82,18 @@ local icon_place_map = {
bottom = 4,
}
local align_map = {
left = 0,
center = 1,
right = 2,
}
local valign_map = {
top = 0,
center = 1,
bottom = 2,
}
local function opt_color(val, def)
assert(val == nil or core.colorspec_to_int(val))
return val or def
@ -130,6 +142,22 @@ local function cascade_layer(new, add, props, p)
ui._opt(add[p.."_frame_time"], "number", props[p.."_frame_time"])
end
local function cascade_text(new, add, props)
new.prepend = ui._opt(add.prepend, "string", props.prepend)
new.append = ui._opt(add.append, "string", props.append)
new.text_color = opt_color(add.text_color, props.text_color)
new.text_mark = opt_color(add.text_mark, props.text_mark)
new.text_size = ui._opt(add.text_size, "number", props.text_size)
new.text_mono = ui._opt(add.text_mono, "boolean", props.text_mono)
new.text_italic = ui._opt(add.text_italic, "boolean", props.text_italic)
new.text_bold = ui._opt(add.text_bold, "boolean", props.text_bold)
new.text_align = ui._opt_enum(add.text_align, align_map, props.text_align)
new.text_valign = ui._opt_enum(add.text_valign, valign_map, props.text_valign)
end
function ui._cascade_props(add, props)
local new = {}
@ -148,6 +176,8 @@ function ui._cascade_props(add, props)
new.icon_gutter = ui._opt(add.icon_gutter, "number", props.icon_gutter)
new.icon_overlap = ui._opt(add.icon_overlap, "boolean", props.icon_overlap)
cascade_text(new, add, props)
return new
end
@ -243,6 +273,40 @@ local function encode_layer(props, p)
return fl
end
local function encode_text(props)
local fl = ui._make_flags()
if ui._shift_flag(fl, props.prepend) then
ui._encode_flag(fl, "s", props.prepend)
end
if ui._shift_flag(fl, props.append) then
ui._encode_flag(fl, "s", props.append)
end
if ui._shift_flag(fl, props.text_color) then
ui._encode_flag(fl, "I", core.colorspec_to_int(props.text_color))
end
if ui._shift_flag(fl, props.text_mark) then
ui._encode_flag(fl, "I", core.colorspec_to_int(props.text_mark))
end
if ui._shift_flag(fl, props.text_size) then
ui._encode_flag(fl, "I", props.text_size)
end
ui._shift_flag_bool(fl, props.text_mono)
ui._shift_flag_bool(fl, props.text_italic)
ui._shift_flag_bool(fl, props.text_bold)
if ui._shift_flag(fl, props.text_align) then
ui._encode_flag(fl, "B", align_map[props.text_align])
end
if ui._shift_flag(fl, props.text_valign) then
ui._encode_flag(fl, "B", valign_map[props.text_valign])
end
return fl
end
local function encode_subflags(fl, sub_fl)
if ui._shift_flag(fl, sub_fl.flags ~= 0) then
ui._encode_flag(fl, "s", ui._encode_flags(sub_fl))
@ -277,5 +341,7 @@ function ui._encode_props(props)
end
ui._shift_flag_bool(fl, props.icon_overlap)
encode_subflags(fl, encode_text(props))
return ui._encode("s", ui._encode_flags(fl))
end

View file

@ -7,6 +7,7 @@
#include "debug.h"
#include "log.h"
#include "porting.h"
#include "client/fontengine.h"
#include "ui/elem.h"
#include "ui/manager.h"
#include "ui/window.h"
@ -29,11 +30,16 @@ namespace ui
void Box::reset()
{
m_content.clear();
m_label = "";
m_style.reset();
for (State i = 0; i < m_style_refs.size(); i++) {
m_style_refs[i] = NO_STYLE;
}
m_text = L"";
m_font = nullptr;
}
void Box::read(std::istream &full_is)
@ -93,6 +99,15 @@ namespace ui
}
}
// Now that we have updated text style properties, we can update our
// cached text string and font object.
m_text = utf8_to_wide(m_style.text.prepend) + utf8_to_wide(m_label) +
utf8_to_wide(m_style.text.append);
FontSpec spec(m_style.text.size, m_style.text.mono ? FM_Mono : FM_Standard,
m_style.text.bold, m_style.text.italic);
m_font = g_fontengine->getFont(spec);
// Since our box has been restyled, the previously computed layout
// information is no longer valid.
m_min_layout = SizeF();
@ -140,7 +155,7 @@ namespace ui
{
if (m_style.display != DisplayMode::HIDDEN) {
drawBox();
drawIcon();
drawItems();
}
for (Box *box : m_content) {
@ -294,6 +309,11 @@ namespace ui
void Box::resizeBox()
{
// First, we need to expand the minimum size of the box to accommodate
// the size of any text it might contain.
SizeF text_size = getWindow().getTextSize(m_font, m_text);
m_min_content = m_min_content.max(text_size);
// 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) {
@ -627,13 +647,19 @@ namespace ui
}
}
void Box::drawIcon()
void Box::drawItems()
{
// 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);
// The window handles all the complicated text layout, so we can just
// draw the text with all the appropriate styling.
getWindow().drawText(m_content_rect, m_clip_rect, m_font, m_text,
m_style.text.color, m_style.text.mark,
m_style.text.align, m_style.text.valign);
}
bool Box::isHovered() const

View file

@ -58,10 +58,15 @@ namespace ui
u32 m_item;
std::vector<Box *> m_content;
std::string_view m_label;
Style m_style;
std::array<u32, NUM_STATES> m_style_refs;
// We cache the font and text content every time the box is restyled.
std::wstring m_text;
gui::IGUIFont *m_font;
// Cached information about the layout of the box, which is cleared in
// restyle() and recomputed in resize() and relayout().
SizeF m_min_layout;
@ -97,6 +102,9 @@ namespace ui
const std::vector<Box *> &getContent() const { return m_content; }
void setContent(std::vector<Box *> content) { m_content = std::move(content); }
std::string_view getLabel() const { return m_label; }
void setLabel(std::string_view label) { m_label = label; }
void reset();
void read(std::istream &is);
@ -125,7 +133,7 @@ namespace ui
void relayoutPlace();
void drawBox();
void drawIcon();
void drawItems();
bool isHovered() const;
bool isPressed() const;

View file

@ -61,6 +61,7 @@ namespace ui
m_parent = nullptr;
m_children.clear();
m_label = "";
m_main_box.reset();
m_events = 0;
@ -72,6 +73,8 @@ namespace ui
if (testShift(set_mask))
readChildren(is);
if (testShift(set_mask))
m_label = readStr16(is);
if (testShift(set_mask))
m_main_box.read(is);
@ -79,7 +82,9 @@ namespace ui
for (Elem *elem : m_children) {
content.push_back(&elem->getMain());
}
m_main_box.setContent(std::move(content));
m_main_box.setLabel(m_label);
}
bool Elem::isFocused() const

View file

@ -49,6 +49,8 @@ namespace ui
Elem *m_parent;
std::vector<Elem *> m_children;
std::string m_label;
Box m_main_box;
u64 m_hovered_box = Box::NO_ID; // Persistent
u64 m_pressed_box = Box::NO_ID; // Persistent

View file

@ -8,6 +8,7 @@
#include "util/serialize.h"
#include <dimension2d.h>
#include <IGUIFont.h>
#include <ITexture.h>
#include <rect.h>
#include <vector2d.h>

View file

@ -43,6 +43,14 @@ namespace ui
return (IconPlace)place;
}
static Align toAlign(u8 align)
{
if (align > (u8)Align::MAX) {
return Align::CENTER;
}
return (Align)align;
}
void Layout::reset()
{
type = LayoutType::PLACE;
@ -134,6 +142,50 @@ namespace ui
frame_time = std::max(readU32(is), 1U);
}
void Text::reset()
{
prepend = "";
append = "";
color = WHITE;
mark = BLANK;
size = 16;
mono = false;
italic = false;
bold = false;
align = Align::CENTER;
valign = Align::CENTER;
}
void Text::read(std::istream &full_is)
{
auto is = newIs(readStr16(full_is));
u32 set_mask = readU32(is);
if (testShift(set_mask))
prepend = readStr16(is);
if (testShift(set_mask))
append = readStr16(is);
if (testShift(set_mask))
color = readARGB8(is);
if (testShift(set_mask))
mark = readARGB8(is);
if (testShift(set_mask))
size = std::clamp(readU32(is), 1U, 999U);
testShiftBool(set_mask, mono);
testShiftBool(set_mask, italic);
testShiftBool(set_mask, bold);
if (testShift(set_mask))
align = toAlign(readU8(is));
if (testShift(set_mask))
valign = toAlign(readU8(is));
}
void Style::reset()
{
layout.reset();
@ -150,6 +202,8 @@ namespace ui
icon_place = IconPlace::CENTER;
icon_gutter = 0.0f;
icon_overlap = false;
text.reset();
}
void Style::read(std::istream &is)
@ -181,5 +235,8 @@ namespace ui
if (testShift(set_mask))
icon_gutter = readF32(is);
testShiftBool(set_mask, icon_overlap);
if (testShift(set_mask))
text.read(is);
}
}

View file

@ -53,6 +53,16 @@ namespace ui
MAX = BOTTOM,
};
// Serialized enum; do not change order of entries.
enum class Align : u8
{
START,
CENTER,
END,
MAX = END,
};
struct Layout
{
LayoutType type;
@ -101,6 +111,28 @@ namespace ui
void read(std::istream &is);
};
struct Text
{
std::string prepend;
std::string append;
video::SColor color;
video::SColor mark;
u32 size;
bool mono;
bool italic;
bool bold;
Align align;
Align valign;
Text() { reset(); }
void reset();
void read(std::istream &is);
};
struct Style
{
Layout layout;
@ -118,6 +150,8 @@ namespace ui
float icon_gutter;
bool icon_overlap;
Text text;
Style() { reset(); }
void reset();

View file

@ -162,6 +162,41 @@ namespace ui
return PosF(x, y) / getScale();
}
SizeF Window::getTextSize(gui::IGUIFont *font, std::wstring_view text)
{
// If we have an empty string, we want it to take up no space. IGUIFont
// measures the dimensions of an empty string as having the normal line
// height rather than no space.
if (text.empty()) {
return SizeF();
}
// IGUIFont measures the height of text with newlines incorrectly, so
// we have to measure each line in the string individually.
SizeF text_size = SizeF();
size_t start = 0;
while (start <= text.size()) {
// Get the line spanning from the start of this line to the next
// newline, or the end of the string if there are no more newlines.
size_t end = std::min(text.find(L'\n', start), text.size());
std::wstring line(text.substr(start, end - start));
// Get the dimensions of the line. Since fonts are already scaled,
// we have to reverse the scaling factor to get the right size.
SizeF line_size = SizeF(font->getDimension(
unescape_enriched(line).c_str())) / getScale();
text_size.W = std::max(text_size.W, line_size.W);
text_size.H += line_size.H;
// Move the start of the current line to after the end of this one.
start = end + 1;
}
return text_size;
}
void Window::drawRect(RectF dst, RectF clip, video::SColor color)
{
if (dst.intersectWith(clip).empty() || color.getAlpha() == 0) {
@ -189,6 +224,81 @@ namespace ui
src * DispF(getTextureSize(texture)), &scaled_clip, colors, true);
}
void Window::drawText(RectF dst, RectF clip, gui::IGUIFont *font,
std::wstring_view text, video::SColor color, video::SColor mark,
Align align, Align valign)
{
if (dst.intersectWith(clip).empty() || font == nullptr || text.empty()) {
return;
}
// We count the number of lines in the text to find the total height.
size_t num_lines = std::count(text.begin(), text.end(), L'\n') + 1;
// Get the height of a single line, and use this with the vertical
// alignment to find the vertical position of the first line.
float height = font->getDimension(L"").Height / getScale();
float top;
switch (valign) {
case Align::START:
top = dst.T;
break;
case Align::CENTER:
top = (dst.T + dst.B - (height * num_lines)) / 2.0f;
break;
case Align::END:
top = dst.B - (height * num_lines);
break;
}
core::recti scaled_clip = clip * getScale();
// Like getTextSize(), we loop over each line in the string.
size_t start = 0;
while (start <= text.size()) {
size_t end = std::min(text.find(L'\n', start), text.size());
std::wstring line(text.substr(start, end - start));
// Get the width of this line of text. Just like the height, we use
// the alignment to find the horizontal position for this line.
float width = font->getDimension(
unescape_enriched(line).c_str()).Width / getScale();
float left;
switch (align) {
case Align::START:
left = dst.L;
break;
case Align::CENTER:
left = (dst.L + dst.R - width) / 2.0f;
break;
case Align::END:
left = dst.R - width;
break;
}
// This gives us the destination rect for this line of the text,
// which we scale appropriately.
RectF line_rect = RectF(PosF(left, top), SizeF(width, height)) * getScale();
// If we have a highlight color for the text, draw the highlight
// before we draw the line.
if (mark.getAlpha() != 0) {
RenderingEngine::get_video_driver()->draw2DRectangle(
mark, line_rect, &scaled_clip);
}
// Then draw the text itself using the provided font.
font->draw(line.c_str(), line_rect, color, false, false, &scaled_clip);
// Finally, advance to the next line.
top += height;
start = end + 1;
}
}
void Window::drawAll()
{
Box &backdrop = m_root_elem->getBackdrop();

View file

@ -98,9 +98,14 @@ namespace ui
SizeF getScreenSize() const;
PosF getPointerPos() const;
SizeF getTextSize(gui::IGUIFont *font, std::wstring_view text);
void drawRect(RectF dst, RectF clip, video::SColor color);
void drawTexture(RectF dst, RectF clip, video::ITexture *texture,
RectF src = RectF(0.0f, 0.0f, 1.0f, 1.0f), video::SColor tint = WHITE);
void drawText(RectF dst, RectF clip, gui::IGUIFont *font, std::wstring_view text,
video::SColor color = WHITE, video::SColor mark = BLANK,
Align align = Align::START, Align valign = Align::START);
void drawAll();