diff --git a/builtin/ui/clickable_elems.lua b/builtin/ui/clickable_elems.lua new file mode 100644 index 000000000..f2fe71ed5 --- /dev/null +++ b/builtin/ui/clickable_elems.lua @@ -0,0 +1,106 @@ +-- Luanti +-- SPDX-License-Identifier: LGPL-2.1-or-later +-- Copyright (C) 2024 v-rob, Vincent Robinson + +ui.Button = ui._new_type(ui.Elem, "button", 0x02, true) + +function ui.Button:_init(props) + ui.Elem._init(self, props) + + self._disabled = ui._opt(props.disabled, "boolean") + self._on_press = ui._opt(props.on_press, "function") +end + +function ui.Button:_encode_fields() + local fl = ui._make_flags() + + ui._shift_flag(fl, self._disabled) + ui._shift_flag(fl, self._on_press) + + return ui._encode("SZ", ui.Elem._encode_fields(self), ui._encode_flags(fl)) +end + +ui.Button._handlers[0x00] = function(self, ev, data) + return self._on_press +end + +ui.Toggle = ui._new_type(ui.Elem, "toggle", 0x03, true) + +ui.Check = ui.derive_elem(ui.Toggle, "check") +ui.Switch = ui.derive_elem(ui.Toggle, "switch") + +function ui.Toggle:_init(props) + ui.Elem._init(self, props) + + self._disabled = ui._opt(props.disabled, "boolean") + self._selected = ui._opt(props.selected, "boolean") + + self._on_press = ui._opt(props.on_press, "function") + self._on_change = ui._opt(props.on_change, "function") +end + +function ui.Toggle:_encode_fields() + local fl = ui._make_flags() + + ui._shift_flag(fl, self._disabled) + ui._shift_flag_bool(fl, self._selected) + + ui._shift_flag(fl, self._on_press) + ui._shift_flag(fl, self._on_change) + + return ui._encode("SZ", ui.Elem._encode_fields(self), ui._encode_flags(fl)) +end + +ui.Toggle._handlers[0x00] = function(self, ev, data) + return self._on_press +end + +ui.Toggle._handlers[0x01] = function(self, ev, data) + local selected = ui._decode("B", data) + ev.selected = selected ~= 0 + + return self._on_change +end + +ui.Option = ui._new_type(ui.Elem, "option", 0x04, true) + +ui.Radio = ui.derive_elem(ui.Option, "radio") + +function ui.Option:_init(props) + ui.Elem._init(self, props) + + self._disabled = ui._opt(props.disabled, "boolean") + self._selected = ui._opt(props.selected, "boolean") + + self._family = ui._opt(props.family, "id") + + self._on_press = ui._opt(props.on_press, "function") + self._on_change = ui._opt(props.on_change, "function") +end + +function ui.Option:_encode_fields() + local fl = ui._make_flags() + + ui._shift_flag(fl, self._disabled) + ui._shift_flag_bool(fl, self._selected) + + if ui._shift_flag(fl, self._family) then + ui._encode_flag(fl, "z", self._family) + end + + ui._shift_flag(fl, self._on_press) + ui._shift_flag(fl, self._on_change) + + return ui._encode("SZ", ui.Elem._encode_fields(self), ui._encode_flags(fl)) +end + +ui.Option._handlers[0x00] = function(self, ev, data) + return self._on_press +end + +ui.Option._handlers[0x01] = function(self, ev, data) + local selected = ui._decode("B", data) + ev.selected = selected ~= 0 + + return self._on_change +end diff --git a/builtin/ui/init.lua b/builtin/ui/init.lua index 1f46d470b..56f87277b 100644 --- a/builtin/ui/init.lua +++ b/builtin/ui/init.lua @@ -11,6 +11,7 @@ dofile(UI_PATH .. "selector.lua") dofile(UI_PATH .. "style.lua") dofile(UI_PATH .. "elem.lua") +dofile(UI_PATH .. "clickable_elems.lua") dofile(UI_PATH .. "static_elems.lua") dofile(UI_PATH .. "window.lua") diff --git a/builtin/ui/selector.lua b/builtin/ui/selector.lua index a1db3225d..c89b2087c 100644 --- a/builtin/ui/selector.lua +++ b/builtin/ui/selector.lua @@ -246,6 +246,19 @@ func_preds["nth_last_match"] = function(str) end end +func_preds["family"] = function(family) + if family == "*" then + return function(elem) + return result(elem._family ~= nil) + end + end + + assert(ui.is_id(family), "Expected '*' or ID string for ?family()") + return function(elem) + return result(elem._family == family) + end +end + local function parse_term(str, pred) str = str:trim() assert(str ~= "", "Expected selector term") diff --git a/builtin/ui/theme.lua b/builtin/ui/theme.lua index 9aa761335..1079dd30c 100644 --- a/builtin/ui/theme.lua +++ b/builtin/ui/theme.lua @@ -19,6 +19,10 @@ local prelude_theme = ui.Style { ui.Style "image" { icon_scale = 0, }, + ui.Style "check, switch, radio" { + icon_place = "left", + text_align = "left", + }, } function ui.get_prelude_theme() diff --git a/src/ui/CMakeLists.txt b/src/ui/CMakeLists.txt index e18b52ae7..f83ac19c4 100644 --- a/src/ui/CMakeLists.txt +++ b/src/ui/CMakeLists.txt @@ -1,5 +1,6 @@ set(ui_SRCS ${CMAKE_CURRENT_SOURCE_DIR}/box.cpp + ${CMAKE_CURRENT_SOURCE_DIR}/clickable_elems.cpp ${CMAKE_CURRENT_SOURCE_DIR}/elem.cpp ${CMAKE_CURRENT_SOURCE_DIR}/manager.cpp ${CMAKE_CURRENT_SOURCE_DIR}/static_elems.cpp diff --git a/src/ui/clickable_elems.cpp b/src/ui/clickable_elems.cpp new file mode 100644 index 000000000..49a7934ea --- /dev/null +++ b/src/ui/clickable_elems.cpp @@ -0,0 +1,173 @@ +// Luanti +// SPDX-License-Identifier: LGPL-2.1-or-later +// Copyright (C) 2024 v-rob, Vincent Robinson + +#include "ui/clickable_elems.h" + +#include "debug.h" +#include "log.h" +#include "ui/manager.h" +#include "util/serialize.h" + +namespace ui +{ + void Button::reset() + { + Elem::reset(); + + m_disabled = false; + } + + void Button::read(std::istream &is) + { + auto super = newIs(readStr32(is)); + Elem::read(super); + + u32 set_mask = readU32(is); + + m_disabled = testShift(set_mask); + + if (testShift(set_mask)) + enableEvent(ON_PRESS); + } + + bool Button::processInput(const SDL_Event &event) + { + return getMain().processFullPress(event, UI_CALLBACK(onPress)); + } + + void Button::onPress() + { + if (!m_disabled && testEvent(ON_PRESS)) { + g_manager.sendMessage(createEvent(ON_PRESS).str()); + } + } + + void Toggle::reset() + { + Elem::reset(); + + m_disabled = false; + } + + void Toggle::read(std::istream &is) + { + auto super = newIs(readStr32(is)); + Elem::read(super); + + u32 set_mask = readU32(is); + + m_disabled = testShift(set_mask); + testShiftBool(set_mask, m_selected); + + if (testShift(set_mask)) + enableEvent(ON_PRESS); + if (testShift(set_mask)) + enableEvent(ON_CHANGE); + } + + bool Toggle::processInput(const SDL_Event &event) + { + return getMain().processFullPress(event, UI_CALLBACK(onPress)); + } + + void Toggle::onPress() + { + if (m_disabled) { + return; + } + + m_selected = !m_selected; + + // Send both a press and a change event since both occurred. + if (testEvent(ON_PRESS)) { + g_manager.sendMessage(createEvent(ON_PRESS).str()); + } + if (testEvent(ON_CHANGE)) { + auto os = createEvent(ON_CHANGE); + writeU8(os, m_selected); + + g_manager.sendMessage(os.str()); + } + } + + void Option::reset() + { + Elem::reset(); + + m_disabled = false; + m_family.clear(); + } + + void Option::read(std::istream &is) + { + auto super = newIs(readStr32(is)); + Elem::read(super); + + u32 set_mask = readU32(is); + + m_disabled = testShift(set_mask); + testShiftBool(set_mask, m_selected); + + if (testShift(set_mask)) + m_family = readNullStr(is); + + if (testShift(set_mask)) + enableEvent(ON_PRESS); + if (testShift(set_mask)) + enableEvent(ON_CHANGE); + } + + bool Option::processInput(const SDL_Event &event) + { + return getMain().processFullPress(event, UI_CALLBACK(onPress)); + } + + void Option::onPress() + { + if (m_disabled) { + return; + } + + // Send a press event for this pressed option button. + if (testEvent(ON_PRESS)) { + g_manager.sendMessage(createEvent(ON_PRESS).str()); + } + + // Select this option button unconditionally before deselecting the + // others in the family. + onChange(true); + + // If this option button has no family, then don't do anything else + // since there may be other buttons with the same empty family string. + if (m_family.empty()) { + return; + } + + // If we find any other option buttons in this family, deselect them. + for (Elem *elem : getWindow().getElems()) { + if (elem->getType() != getType()) { + continue; + } + + Option *option = (Option *)elem; + if (option->m_family == m_family && option != this) { + option->onChange(false); + } + } + } + + void Option::onChange(bool selected) + { + bool was_selected = m_selected; + m_selected = selected; + + // If the state of the option button changed, send a change event. + if (was_selected != m_selected && testEvent(ON_CHANGE)) { + auto os = createEvent(ON_CHANGE); + writeU8(os, m_selected); + + g_manager.sendMessage(os.str()); + } + } +} diff --git a/src/ui/clickable_elems.h b/src/ui/clickable_elems.h new file mode 100644 index 000000000..57e1f4ac6 --- /dev/null +++ b/src/ui/clickable_elems.h @@ -0,0 +1,102 @@ +// Luanti +// SPDX-License-Identifier: LGPL-2.1-or-later +// Copyright (C) 2024 v-rob, Vincent Robinson + +#pragma once + +#include "ui/box.h" +#include "ui/elem.h" +#include "ui/helpers.h" + +#include +#include + +namespace ui +{ + class Button : public Elem + { + private: + // Serialized constants; do not change values of entries. + static constexpr u32 ON_PRESS = 0x00; + + bool m_disabled; + + public: + Button(Window &window, std::string id) : + Elem(window, std::move(id)) + {} + + virtual Type getType() const override { return BUTTON; } + + virtual void reset() override; + virtual void read(std::istream &is) override; + + virtual bool isBoxDisabled(const Box &box) const override { return m_disabled; } + + virtual bool processInput(const SDL_Event &event) override; + + private: + void onPress(); + }; + + class Toggle : public Elem + { + private: + // Serialized constants; do not change values of entries. + static constexpr u32 ON_PRESS = 0x00; + static constexpr u32 ON_CHANGE = 0x01; + + bool m_disabled; + bool m_selected = false; // Persistent + + public: + Toggle(Window &window, std::string id) : + Elem(window, std::move(id)) + {} + + virtual Type getType() const override { return TOGGLE; } + + virtual void reset() override; + virtual void read(std::istream &is) override; + + virtual bool isBoxSelected(const Box &box) const override { return m_selected; } + virtual bool isBoxDisabled(const Box &box) const override { return m_disabled; } + + virtual bool processInput(const SDL_Event &event) override; + + private: + void onPress(); + }; + + class Option : public Elem + { + private: + // Serialized constants; do not change values of entries. + static constexpr u32 ON_PRESS = 0x00; + static constexpr u32 ON_CHANGE = 0x01; + + bool m_disabled; + std::string m_family; + + bool m_selected = false; // Persistent + + public: + Option(Window &window, std::string id) : + Elem(window, std::move(id)) + {} + + virtual Type getType() const override { return OPTION; } + + virtual void reset() override; + virtual void read(std::istream &is) override; + + virtual bool isBoxSelected(const Box &box) const override { return m_selected; } + virtual bool isBoxDisabled(const Box &box) const override { return m_disabled; } + + virtual bool processInput(const SDL_Event &event) override; + + private: + void onPress(); + void onChange(bool selected); + }; +} diff --git a/src/ui/elem.cpp b/src/ui/elem.cpp index 82bffedc4..64dea6d5f 100644 --- a/src/ui/elem.cpp +++ b/src/ui/elem.cpp @@ -11,6 +11,7 @@ #include "util/serialize.h" // Include every element header for Elem::create() +#include "ui/clickable_elems.h" #include "ui/static_elems.h" #include @@ -29,6 +30,9 @@ namespace ui switch (type) { CREATE(ELEM, Elem); CREATE(ROOT, Root); + CREATE(BUTTON, Button); + CREATE(TOGGLE, Toggle); + CREATE(OPTION, Option); default: return nullptr; } diff --git a/src/ui/elem.h b/src/ui/elem.h index fdeb49c23..1dca2635d 100644 --- a/src/ui/elem.h +++ b/src/ui/elem.h @@ -32,6 +32,9 @@ namespace ui { ELEM = 0x00, ROOT = 0x01, + BUTTON = 0x02, + TOGGLE = 0x03, + OPTION = 0x04, }; // The main box is always the zeroth item in the Box::NO_GROUP group.