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:
parent
bb2f857b04
commit
c7fca1b956
11 changed files with 323 additions and 3 deletions
|
@ -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)
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
10
src/ui/box.h
10
src/ui/box.h
|
@ -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;
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -8,6 +8,7 @@
|
|||
#include "util/serialize.h"
|
||||
|
||||
#include <dimension2d.h>
|
||||
#include <IGUIFont.h>
|
||||
#include <ITexture.h>
|
||||
#include <rect.h>
|
||||
#include <vector2d.h>
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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();
|
||||
|
|
|
@ -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();
|
||||
|
|
|
@ -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();
|
||||
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue