mirror of
https://github.com/TwilitRealm/dusklight
synced 2026-08-23 22:10:36 -04:00
UiService v2.1: Virtualized lists (#2342)
* UiService: Add `pane_add_list` * Fix CSS scoping
This commit is contained in:
@@ -496,6 +496,49 @@ controls are only available inside window tabs.
|
||||
and calls the group's build callback with that pane, which is useful for organizing related controls without adding
|
||||
more tabs.
|
||||
|
||||
**Lists:** `pane_add_list` adds a scrollable virtualized list of items that can be efficiently updated and filtered.
|
||||
Keys must be unique and remain stable across replacements.
|
||||
|
||||
```cpp
|
||||
UiListHandle locationList = 0;
|
||||
|
||||
void replace_locations(std::string_view query) {
|
||||
std::vector<UiListItem> items;
|
||||
for (uint64_t i = 0; i < locations.size(); ++i) {
|
||||
if (matches(locations[i], query)) {
|
||||
UiListItem item = UI_LIST_ITEM_INIT;
|
||||
item.key = i;
|
||||
item.label = locations[i].c_str();
|
||||
items.push_back(item);
|
||||
}
|
||||
}
|
||||
svc_ui->list_set_items(mod_ctx, locationList, items.data(), items.size());
|
||||
}
|
||||
|
||||
void set_filter(ModContext*, void*, const UiControlValue* value) {
|
||||
replace_locations(value->string_value);
|
||||
}
|
||||
|
||||
bool location_selected(ModContext*, UiListHandle, uint64_t key, void*) {
|
||||
return selected_locations.contains(key);
|
||||
}
|
||||
|
||||
UiControlDesc filter = UI_CONTROL_DESC_INIT;
|
||||
filter.kind = UI_CONTROL_STRING;
|
||||
filter.label = "Filter";
|
||||
filter.get = get_filter;
|
||||
filter.set = set_filter;
|
||||
filter.string_set_mode = UI_STRING_SET_ON_CHANGE; /* invoke `set` while typing */
|
||||
svc_ui->pane_add_control(mod_ctx, pane, &filter, nullptr);
|
||||
|
||||
UiListDesc list = UI_LIST_DESC_INIT;
|
||||
/* items may be passed as a part of list creation, or set afterwards */
|
||||
list.on_pressed = location_pressed;
|
||||
list.is_selected = location_selected;
|
||||
svc_ui->pane_add_list(mod_ctx, pane, &list, &locationList);
|
||||
replace_locations(""); /* calls `list_set_items` */
|
||||
```
|
||||
|
||||
**Windows:** `window_push` pushes a tabbed two-pane window onto the document stack and shows it. Each tab's `build`
|
||||
receives the window handle plus fresh left and right pane handles on every activation. The optional per-tab `update`
|
||||
runs each frame while that tab is active. `on_closed` fires when the window is destroyed. `desc.rcss` optionally styles
|
||||
|
||||
Vendored
+1
-1
Submodule extern/aurora updated: 0f15fb65d1...2c9c95cfed
@@ -1548,6 +1548,8 @@ set(DUSK_FILES
|
||||
src/dusk/ui/input.hpp
|
||||
src/dusk/ui/logs_window.cpp
|
||||
src/dusk/ui/logs_window.hpp
|
||||
src/dusk/ui/list.cpp
|
||||
src/dusk/ui/list.hpp
|
||||
src/dusk/ui/menu_bar.cpp
|
||||
src/dusk/ui/menu_bar.hpp
|
||||
src/dusk/ui/mod_texture_provider.cpp
|
||||
|
||||
+70
-13
@@ -107,7 +107,6 @@ window content pane {
|
||||
min-width: 0;
|
||||
min-height: 0;
|
||||
padding: 24dp;
|
||||
padding-bottom: 0dp;
|
||||
gap: 8dp;
|
||||
overflow: hidden auto;
|
||||
font-size: 20dp;
|
||||
@@ -121,6 +120,76 @@ window content pane > * {
|
||||
flex: 0 0 auto;
|
||||
}
|
||||
|
||||
ui-list {
|
||||
display: flex;
|
||||
flex-flow: column;
|
||||
flex: 1 1 0;
|
||||
min-width: 0;
|
||||
min-height: 0;
|
||||
}
|
||||
|
||||
window content pane > ui-list,
|
||||
.modal-content pane > ui-list {
|
||||
flex: 1 1 0;
|
||||
min-width: 0;
|
||||
min-height: 0;
|
||||
}
|
||||
|
||||
ui-list-viewport {
|
||||
display: block;
|
||||
flex: 1 1 0;
|
||||
min-width: 0;
|
||||
min-height: 0;
|
||||
overflow: hidden auto;
|
||||
}
|
||||
|
||||
ui-list-content {
|
||||
display: flex;
|
||||
flex-flow: column;
|
||||
gap: 8dp;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
ui-list-content > button.ui-list-row {
|
||||
flex: 0 0 auto;
|
||||
}
|
||||
|
||||
ui-list-empty {
|
||||
display: block;
|
||||
padding: 16dp;
|
||||
text-align: center;
|
||||
opacity: 0.45;
|
||||
}
|
||||
|
||||
window content pane > ui-list {
|
||||
margin-left: -24dp;
|
||||
margin-right: -24dp;
|
||||
}
|
||||
|
||||
window content pane > ui-list:first-child {
|
||||
margin-top: -24dp;
|
||||
}
|
||||
|
||||
window content pane > ui-list:last-child {
|
||||
margin-bottom: -24dp;
|
||||
}
|
||||
|
||||
window content pane > ui-list ui-list-content,
|
||||
window content pane > ui-list ui-list-empty {
|
||||
padding-left: 24dp;
|
||||
padding-right: 24dp;
|
||||
}
|
||||
|
||||
window content pane > ui-list:first-child ui-list-content,
|
||||
window content pane > ui-list:first-child ui-list-empty {
|
||||
padding-top: 24dp;
|
||||
}
|
||||
|
||||
window content pane > ui-list:last-child ui-list-content,
|
||||
window content pane > ui-list:last-child ui-list-empty {
|
||||
padding-bottom: 24dp;
|
||||
}
|
||||
|
||||
window content pane:last-of-type > div {
|
||||
line-height: 1.625;
|
||||
}
|
||||
@@ -131,14 +200,6 @@ window content pane:last-of-type > div {
|
||||
color: rgba(224, 219, 200, 65%);
|
||||
}
|
||||
|
||||
window content pane > spacer {
|
||||
display: block;
|
||||
/* Completes the 24dp bottom inset after the pane's 8dp gap. */
|
||||
flex: 0 0 16dp;
|
||||
height: 16dp;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
scrollbarvertical {
|
||||
width: 8dp;
|
||||
margin: 4dp 4dp 4dp 0;
|
||||
@@ -567,10 +628,6 @@ window.modal.danger .modal-header icon {
|
||||
flex: 0 0 auto;
|
||||
}
|
||||
|
||||
.modal-content pane > spacer {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.verification-progress {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
|
||||
@@ -9,7 +9,7 @@
|
||||
|
||||
#define UI_SERVICE_ID "dev.twilitrealm.dusklight.ui"
|
||||
#define UI_SERVICE_MAJOR 2u
|
||||
#define UI_SERVICE_MINOR 0u
|
||||
#define UI_SERVICE_MINOR 1u
|
||||
|
||||
/*
|
||||
* UI primitives: a panel inside the host Mods window, mod-owned windows, dialogs, toasts,
|
||||
@@ -66,6 +66,11 @@ typedef enum UiControlBinding {
|
||||
UI_BINDING_CONFIG_VAR = 1,
|
||||
} UiControlBinding;
|
||||
|
||||
typedef enum UiStringSetMode {
|
||||
UI_STRING_SET_ON_COMMIT = 0, /* invokes `set` when input is committed */
|
||||
UI_STRING_SET_ON_CHANGE = 1, /* invokes `set` on every text change (e.g. while typing) */
|
||||
} UiStringSetMode;
|
||||
|
||||
/* Tagged by the control's kind: TOGGLE reads bool_value, NUMBER and SELECT read int_value, STRING
|
||||
* and COLOR read string_value. string_value passed to a setter is only valid during the call; a
|
||||
* getter should point it at storage owned by the mod (e.g. a static buffer) that stays valid until
|
||||
@@ -120,13 +125,44 @@ typedef struct UiControlDesc {
|
||||
/* COLOR: optional RRGGBB/RRGGBBAA values for presets. "rainbow" is a special value. */
|
||||
const char* const* color_presets;
|
||||
size_t color_preset_count;
|
||||
bool color_alpha; /* COLOR: use RRGGBBAA values instead of RRGGBB */
|
||||
UiPredicateFn is_selected; /* BUTTON/GROUP: pptional selected state */
|
||||
bool color_alpha; /* COLOR: use RRGGBBAA values instead of RRGGBB */
|
||||
UiPredicateFn is_selected; /* BUTTON/GROUP: optional selected state */
|
||||
UiStringSetMode string_set_mode; /* STRING: when to invoke the setter */
|
||||
} UiControlDesc;
|
||||
|
||||
#define UI_CONTROL_DESC_INIT \
|
||||
{sizeof(UiControlDesc), UI_CONTROL_BUTTON, NULL, NULL, UI_BINDING_CALLBACKS, 0u, NULL, NULL, \
|
||||
NULL, NULL, NULL, NULL, 0, 0, 1, NULL, NULL, NULL, 0u, 0, NULL, 0u, false, NULL}
|
||||
NULL, NULL, NULL, NULL, 0, 0, 1, NULL, NULL, NULL, 0u, 0, NULL, 0u, false, NULL, \
|
||||
UI_STRING_SET_ON_COMMIT}
|
||||
|
||||
typedef uint64_t UiListHandle;
|
||||
|
||||
/* Must be initialized with UI_LIST_ITEM_INIT */
|
||||
typedef struct UiListItem {
|
||||
uint32_t struct_size;
|
||||
uint64_t key; /* required; unique, stable item key */
|
||||
const char* label; /* required; visible item text */
|
||||
} UiListItem;
|
||||
|
||||
#define UI_LIST_ITEM_INIT {sizeof(UiListItem), 0u, NULL}
|
||||
|
||||
typedef void (*UiListPressedFn)(
|
||||
ModContext* ctx, UiListHandle list, uint64_t item_key, void* user_data);
|
||||
typedef bool (*UiListPredicateFn)(
|
||||
ModContext* ctx, UiListHandle list, uint64_t item_key, void* user_data);
|
||||
|
||||
/* Must be initialized with UI_LIST_DESC_INIT */
|
||||
typedef struct UiListDesc {
|
||||
uint32_t struct_size;
|
||||
const UiListItem* items; /* optional; initial set of items */
|
||||
size_t item_count;
|
||||
UiListPressedFn on_pressed; /* required */
|
||||
UiListPredicateFn is_selected; /* optional; polled only for render-visible rows */
|
||||
UiListPredicateFn is_disabled; /* optional; polled only for render-visible rows */
|
||||
void* user_data;
|
||||
} UiListDesc;
|
||||
|
||||
#define UI_LIST_DESC_INIT {sizeof(UiListDesc), NULL, 0u, NULL, NULL, NULL, NULL}
|
||||
|
||||
/* Build pane contents. A non-MOD_OK result fails the mod. */
|
||||
typedef ModResult (*UiPaneBuildFn)(
|
||||
@@ -324,6 +360,13 @@ typedef struct UiService {
|
||||
ModResult (*get_clipboard_text)(
|
||||
ModContext* ctx, char* buffer, size_t bufferSize, size_t* outLength);
|
||||
ModResult (*set_clipboard_text)(ModContext* ctx, const char* text);
|
||||
|
||||
/* A scrollable virtualized list of items. */
|
||||
ModResult (*pane_add_list)(
|
||||
ModContext* ctx, UiElementHandle pane, const UiListDesc* desc, UiListHandle* out_list);
|
||||
/* Replace all items in a list with a new set. */
|
||||
ModResult (*list_set_items)(
|
||||
ModContext* ctx, UiListHandle list, const UiListItem* items, size_t item_count);
|
||||
} UiService;
|
||||
|
||||
MOD_DECLARE_SERVICE(UiService, svc_ui, UI_SERVICE_ID, UI_SERVICE_MAJOR, UI_SERVICE_MINOR);
|
||||
|
||||
+163
-1
@@ -9,6 +9,7 @@
|
||||
#include "dusk/mod_loader.hpp"
|
||||
#include "dusk/mods/loader/loader.hpp"
|
||||
#include "dusk/mods/log_buffer.hpp"
|
||||
#include "dusk/ui/list.hpp"
|
||||
#include "dusk/ui/menu_bar.hpp"
|
||||
#include "dusk/ui/mod_window.hpp"
|
||||
#include "dusk/ui/modal.hpp"
|
||||
@@ -25,16 +26,30 @@
|
||||
#include <cstddef>
|
||||
#include <cstdint>
|
||||
#include <functional>
|
||||
#include <limits>
|
||||
#include <memory>
|
||||
#include <optional>
|
||||
#include <stdexcept>
|
||||
#include <string>
|
||||
#include <string_view>
|
||||
#include <unordered_map>
|
||||
#include <unordered_set>
|
||||
#include <utility>
|
||||
#include <vector>
|
||||
|
||||
#include "SDL3/SDL_clipboard.h"
|
||||
|
||||
namespace {
|
||||
|
||||
constexpr size_t kUiControlSelectedSize =
|
||||
offsetof(UiControlDesc, is_selected) + sizeof(UiPredicateFn);
|
||||
constexpr size_t kUiControlStringSetModeSize =
|
||||
offsetof(UiControlDesc, string_set_mode) + sizeof(UiStringSetMode);
|
||||
constexpr size_t kUiListItemV21Size = offsetof(UiListItem, label) + sizeof(const char*);
|
||||
constexpr size_t kUiListDescV21Size = offsetof(UiListDesc, user_data) + sizeof(void*);
|
||||
|
||||
} // namespace
|
||||
|
||||
namespace dusk::mods::svc::ui_impl {
|
||||
namespace {
|
||||
|
||||
@@ -47,6 +62,7 @@ enum class UiSlotKind : u8 {
|
||||
Text,
|
||||
Progress,
|
||||
Control,
|
||||
List,
|
||||
Style,
|
||||
MenuTab,
|
||||
};
|
||||
@@ -65,6 +81,8 @@ const char* slot_kind_name(UiSlotKind kind) {
|
||||
return "progress";
|
||||
case UiSlotKind::Control:
|
||||
return "control";
|
||||
case UiSlotKind::List:
|
||||
return "list";
|
||||
case UiSlotKind::Style:
|
||||
return "style";
|
||||
case UiSlotKind::MenuTab:
|
||||
@@ -83,6 +101,8 @@ struct UiSlot {
|
||||
// Pane payload
|
||||
ui::Pane* pane = nullptr;
|
||||
ui::Pane* helpPane = nullptr;
|
||||
// List payload
|
||||
ui::List* list = nullptr;
|
||||
// Window/Dialog payload (non-owning; the document stack owns the document)
|
||||
ui::Document* document = nullptr;
|
||||
UiWindowClosedFn onClosed = nullptr;
|
||||
@@ -581,7 +601,7 @@ ModResult ui_pane_add_control(
|
||||
case UI_CONTROL_GROUP:
|
||||
spec.kind = desc.kind == UI_CONTROL_BUTTON ? ui::ModControlSpec::Kind::Button :
|
||||
ui::ModControlSpec::Kind::Group;
|
||||
if (desc.struct_size >= sizeof(UiControlDesc)) {
|
||||
if (desc.struct_size >= kUiControlSelectedSize) {
|
||||
spec.isSelected = wrap_predicate(mod, desc.is_selected, desc.user_data, pane);
|
||||
}
|
||||
spec.onPressed = [modPtr = &mod, fn = desc.on_pressed, userData = desc.user_data,
|
||||
@@ -612,6 +632,8 @@ ModResult ui_pane_add_control(
|
||||
case UI_CONTROL_STRING:
|
||||
spec.kind = ui::ModControlSpec::Kind::String;
|
||||
spec.maxLength = desc.max_length < 1 ? -1 : desc.max_length;
|
||||
spec.stringSetOnChange = desc.struct_size >= kUiControlStringSetModeSize &&
|
||||
desc.string_set_mode == UI_STRING_SET_ON_CHANGE;
|
||||
break;
|
||||
case UI_CONTROL_COLOR:
|
||||
spec.kind = ui::ModControlSpec::Kind::Color;
|
||||
@@ -662,6 +684,69 @@ ModResult ui_pane_add_control(
|
||||
return MOD_OK;
|
||||
}
|
||||
|
||||
ModResult ui_pane_add_list(LoadedMod& mod, uint64_t pane, const UiListDesc& desc,
|
||||
std::vector<ui::List::Item> items, uint64_t& outHandle) {
|
||||
outHandle = 0;
|
||||
auto* paneSlot = resolve(mod, pane, UiSlotKind::Pane, "pane_add_list");
|
||||
if (paneSlot == nullptr) {
|
||||
return MOD_INVALID_ARGUMENT;
|
||||
}
|
||||
auto* paneComponent = paneSlot->pane;
|
||||
|
||||
uint64_t handle = 0;
|
||||
alloc_slot(mod, UiSlotKind::List, handle);
|
||||
|
||||
ui::List::Props props;
|
||||
props.items = std::move(items);
|
||||
props.onPressed = [modPtr = &mod, handle, fn = desc.on_pressed, userData = desc.user_data](
|
||||
uint64_t key) {
|
||||
if (!slot_live(handle)) {
|
||||
return;
|
||||
}
|
||||
guarded_call(*modPtr, "list on_pressed callback",
|
||||
[&] { fn(modPtr->context.get(), handle, key, userData); });
|
||||
};
|
||||
if (desc.is_selected != nullptr) {
|
||||
props.isSelected = [modPtr = &mod, handle, fn = desc.is_selected,
|
||||
userData = desc.user_data](uint64_t key) {
|
||||
if (!slot_live(handle)) {
|
||||
return false;
|
||||
}
|
||||
return guarded_call(*modPtr, "list is_selected callback", false,
|
||||
[&] { return fn(modPtr->context.get(), handle, key, userData); });
|
||||
};
|
||||
}
|
||||
if (desc.is_disabled != nullptr) {
|
||||
props.isDisabled = [modPtr = &mod, handle, fn = desc.is_disabled,
|
||||
userData = desc.user_data](uint64_t key) {
|
||||
if (!slot_live(handle)) {
|
||||
return false;
|
||||
}
|
||||
return guarded_call(*modPtr, "list is_disabled callback", false,
|
||||
[&] { return fn(modPtr->context.get(), handle, key, userData); });
|
||||
};
|
||||
}
|
||||
|
||||
auto& list = paneComponent->add_child<ui::List>(std::move(props));
|
||||
auto* listSlot = slot_from_handle(handle);
|
||||
if (listSlot == nullptr) {
|
||||
return MOD_ERROR;
|
||||
}
|
||||
listSlot->list = &list;
|
||||
track_element(handle, *listSlot, *list.root());
|
||||
outHandle = handle;
|
||||
return MOD_OK;
|
||||
}
|
||||
|
||||
ModResult ui_list_set_items(LoadedMod& mod, uint64_t handle, std::vector<ui::List::Item> items) {
|
||||
auto* slot = resolve(mod, handle, UiSlotKind::List, "list_set_items");
|
||||
if (slot == nullptr || slot->list == nullptr) {
|
||||
return MOD_INVALID_ARGUMENT;
|
||||
}
|
||||
slot->list->set_items(std::move(items));
|
||||
return MOD_OK;
|
||||
}
|
||||
|
||||
ModResult ui_pane_add_group(LoadedMod& mod, uint64_t groupPaneHandle, uint64_t targetPaneHandle,
|
||||
const UiGroupDesc& desc, uint64_t* outElem) {
|
||||
auto* groupSlot = resolve(mod, groupPaneHandle, UiSlotKind::Pane, "pane_add_group");
|
||||
@@ -1175,6 +1260,12 @@ bool valid_control_desc(const UiControlDesc& desc) {
|
||||
default:
|
||||
return false;
|
||||
}
|
||||
if (desc.kind == UI_CONTROL_STRING && desc.struct_size >= kUiControlStringSetModeSize &&
|
||||
desc.string_set_mode != UI_STRING_SET_ON_COMMIT &&
|
||||
desc.string_set_mode != UI_STRING_SET_ON_CHANGE)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
if (desc.kind == UI_CONTROL_SELECT) {
|
||||
if (desc.options == nullptr || desc.option_count == 0) {
|
||||
return false;
|
||||
@@ -1207,6 +1298,36 @@ bool valid_control_desc(const UiControlDesc& desc) {
|
||||
}
|
||||
}
|
||||
|
||||
bool copy_list_items(
|
||||
const UiListItem* items, size_t itemCount, std::vector<ui::List::Item>& outItems) {
|
||||
if (itemCount != 0 && items == nullptr) {
|
||||
return false;
|
||||
}
|
||||
|
||||
std::vector<ui::List::Item> copy;
|
||||
copy.reserve(itemCount);
|
||||
std::unordered_set<uint64_t> keys;
|
||||
keys.reserve(itemCount);
|
||||
auto cursor = reinterpret_cast<uintptr_t>(items);
|
||||
for (size_t i = 0; i < itemCount; ++i) {
|
||||
const auto* item = reinterpret_cast<const UiListItem*>(cursor);
|
||||
const size_t recordSize = item->struct_size;
|
||||
if (recordSize < kUiListItemV21Size || recordSize % alignof(UiListItem) != 0 ||
|
||||
cursor > std::numeric_limits<uintptr_t>::max() - recordSize)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
if (item->label == nullptr || !keys.insert(item->key).second) {
|
||||
return false;
|
||||
}
|
||||
copy.push_back({.key = item->key, .label = item->label});
|
||||
cursor += recordSize;
|
||||
}
|
||||
|
||||
outItems = std::move(copy);
|
||||
return true;
|
||||
}
|
||||
|
||||
ModResult ui_register_mods_panel(ModContext* context, const UiModsPanelDesc* desc) {
|
||||
auto* mod = mod_from_context(context);
|
||||
if (mod == nullptr || desc == nullptr || desc->struct_size < sizeof(UiModsPanelDesc) ||
|
||||
@@ -1273,6 +1394,45 @@ ModResult ui_pane_add_control(ModContext* context, UiElementHandle pane, const U
|
||||
return ui_impl::ui_pane_add_control(*mod, pane, *desc, outElem);
|
||||
}
|
||||
|
||||
ModResult ui_pane_add_list(
|
||||
ModContext* context, UiElementHandle pane, const UiListDesc* desc, UiListHandle* outList) {
|
||||
if (outList != nullptr) {
|
||||
*outList = 0;
|
||||
}
|
||||
auto* mod = mod_from_context(context);
|
||||
if (mod == nullptr || pane == 0 || desc == nullptr || desc->struct_size < kUiListDescV21Size ||
|
||||
desc->on_pressed == nullptr)
|
||||
{
|
||||
return MOD_INVALID_ARGUMENT;
|
||||
}
|
||||
std::vector<ui::List::Item> items;
|
||||
if ((desc->items != nullptr || desc->item_count > 0) &&
|
||||
!copy_list_items(desc->items, desc->item_count, items))
|
||||
{
|
||||
return MOD_INVALID_ARGUMENT;
|
||||
}
|
||||
|
||||
uint64_t handle = 0;
|
||||
const ModResult result = ui_impl::ui_pane_add_list(*mod, pane, *desc, std::move(items), handle);
|
||||
if (result == MOD_OK && outList != nullptr) {
|
||||
*outList = handle;
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
ModResult ui_list_set_items(
|
||||
ModContext* context, UiListHandle list, const UiListItem* items, size_t itemCount) {
|
||||
auto* mod = mod_from_context(context);
|
||||
if (mod == nullptr || list == 0) {
|
||||
return MOD_INVALID_ARGUMENT;
|
||||
}
|
||||
std::vector<ui::List::Item> copiedItems;
|
||||
if (!copy_list_items(items, itemCount, copiedItems)) {
|
||||
return MOD_INVALID_ARGUMENT;
|
||||
}
|
||||
return ui_impl::ui_list_set_items(*mod, list, std::move(copiedItems));
|
||||
}
|
||||
|
||||
ModResult ui_pane_add_group(ModContext* context, UiElementHandle groupPane,
|
||||
UiElementHandle targetPane, const UiGroupDesc* desc, UiElementHandle* outElem) {
|
||||
if (outElem != nullptr) {
|
||||
@@ -1630,6 +1790,8 @@ constexpr UiService s_uiService{
|
||||
.push_toast = ui_push_toast,
|
||||
.get_clipboard_text = ui_get_clipboard_text,
|
||||
.set_clipboard_text = ui_set_clipboard_text,
|
||||
.pane_add_list = ui_pane_add_list,
|
||||
.list_set_items = ui_list_set_items,
|
||||
};
|
||||
|
||||
} // namespace
|
||||
|
||||
@@ -185,8 +185,6 @@ AchievementsWindow::AchievementsWindow() {
|
||||
*confirmingAll = false;
|
||||
clearAllPtr->set_text("Clear All Achievements");
|
||||
});
|
||||
|
||||
pane.finalize();
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,363 @@
|
||||
#include "list.hpp"
|
||||
|
||||
#include "ui.hpp"
|
||||
|
||||
#include "Z2AudioLib/Z2SeMgr.h"
|
||||
#include "m_Do/m_Do_audio.h"
|
||||
|
||||
#include <algorithm>
|
||||
#include <ranges>
|
||||
#include <utility>
|
||||
|
||||
namespace dusk::ui {
|
||||
namespace {
|
||||
|
||||
Rml::Element* create_root(Rml::Element* parent) {
|
||||
auto* document = parent->GetOwnerDocument();
|
||||
auto root = document->CreateElement("ui-list");
|
||||
return parent->AppendChild(std::move(root));
|
||||
}
|
||||
|
||||
Rml::Element* append_element(Rml::Element* parent, const Rml::String& tag) {
|
||||
auto element = parent->GetOwnerDocument()->CreateElement(tag);
|
||||
return parent->AppendChild(std::move(element));
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
List::List(Rml::Element* parent, Props props)
|
||||
: FluentComponent(create_root(parent)), mProps(std::move(props)) {
|
||||
mViewport = append_element(mRoot, "ui-list-viewport");
|
||||
mContent = append_element(mViewport, "ui-list-content");
|
||||
mEmpty = append_element(mRoot, "ui-list-empty");
|
||||
append_text(mEmpty, "No items");
|
||||
|
||||
Component::listen(mViewport, Rml::EventId::Scroll, [this](Rml::Event&) { mCullDirty = true; });
|
||||
listen(Rml::EventId::Keydown, [this](Rml::Event& event) { handle_keydown(event); });
|
||||
|
||||
apply_items(std::move(mProps.items));
|
||||
}
|
||||
|
||||
void List::set_items(std::vector<Item> items) {
|
||||
mPendingSnapshotFocus = capture_snapshot_focus();
|
||||
mPendingItems = std::move(items);
|
||||
}
|
||||
|
||||
void List::update() {
|
||||
if (mPendingItems) {
|
||||
auto items = std::move(*mPendingItems);
|
||||
const auto snapshotFocus = mPendingSnapshotFocus;
|
||||
mPendingItems.reset();
|
||||
mPendingSnapshotFocus.reset();
|
||||
apply_items(std::move(items), snapshotFocus);
|
||||
}
|
||||
|
||||
for (const auto& row : mRows) {
|
||||
if (!row->culled) {
|
||||
row->button->update();
|
||||
}
|
||||
}
|
||||
|
||||
const float scrollTop = mViewport->GetScrollTop();
|
||||
const float viewportWidth = mViewport->GetClientWidth();
|
||||
const float viewportHeight = mViewport->GetClientHeight();
|
||||
if (scrollTop != mLastScrollTop || viewportWidth != mLastViewportWidth ||
|
||||
viewportHeight != mLastViewportHeight)
|
||||
{
|
||||
mCullDirty = true;
|
||||
if (viewportWidth != mLastViewportWidth || viewportHeight != mLastViewportHeight) {
|
||||
mLayoutScanFrames = std::max(mLayoutScanFrames, 2);
|
||||
}
|
||||
mLastScrollTop = scrollTop;
|
||||
mLastViewportWidth = viewportWidth;
|
||||
mLastViewportHeight = viewportHeight;
|
||||
}
|
||||
|
||||
if (mCullDirty || mLayoutScanFrames > 0) {
|
||||
update_culling();
|
||||
mCullDirty = false;
|
||||
if (mLayoutScanFrames > 0) {
|
||||
--mLayoutScanFrames;
|
||||
}
|
||||
}
|
||||
|
||||
update_pending_focus();
|
||||
}
|
||||
|
||||
bool List::focus() {
|
||||
if (mActiveKey) {
|
||||
const int activeIndex = row_index(*mActiveKey);
|
||||
if (activeIndex >= 0 && focus_row(activeIndex, true)) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
for (int i = 0; i < static_cast<int>(mRows.size()); ++i) {
|
||||
if (focus_row(i, true)) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
bool List::focus_from(NavCommand direction) {
|
||||
if (direction != NavCommand::Up) {
|
||||
return focus();
|
||||
}
|
||||
for (int i = static_cast<int>(mRows.size()) - 1; i >= 0; --i) {
|
||||
if (focus_row(i, true)) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
List::Row* List::row_from_element(Rml::Element* element) const {
|
||||
for (const auto& row : mRows) {
|
||||
if (row->button->contains(element)) {
|
||||
return row.get();
|
||||
}
|
||||
}
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
int List::row_index(uint64_t key) const {
|
||||
for (int i = 0; i < static_cast<int>(mRows.size()); ++i) {
|
||||
if (mRows[i]->key == key) {
|
||||
return i;
|
||||
}
|
||||
}
|
||||
return -1;
|
||||
}
|
||||
|
||||
List::SnapshotFocus List::capture_snapshot_focus() {
|
||||
auto* context = mRoot->GetContext();
|
||||
auto* focusedElement = context != nullptr ? context->GetFocusElement() : nullptr;
|
||||
Row* focusedRow = row_from_element(focusedElement);
|
||||
if (focusedRow == nullptr) {
|
||||
return {};
|
||||
}
|
||||
mActiveKey = focusedRow->key;
|
||||
return {
|
||||
.owned = true,
|
||||
.key = focusedRow->key,
|
||||
.index = row_index(focusedRow->key),
|
||||
};
|
||||
}
|
||||
|
||||
std::unique_ptr<List::Row> List::create_row(const Item& item) {
|
||||
auto row = std::make_unique<Row>();
|
||||
row->key = item.key;
|
||||
row->button = std::make_unique<ControlledButton>(mContent,
|
||||
ControlledButton::Props{
|
||||
.text = item.label,
|
||||
.isSelected =
|
||||
[this, key = item.key] { return mProps.isSelected && mProps.isSelected(key); },
|
||||
.isDisabled =
|
||||
[this, key = item.key] { return mProps.isDisabled && mProps.isDisabled(key); },
|
||||
});
|
||||
row->button->root()->SetClass("ui-list-row", true);
|
||||
row->button->root()->SetProperty("visibility", "hidden");
|
||||
row->button->Component::listen(row->button->root(), Rml::EventId::Focus,
|
||||
[this, key = item.key](Rml::Event&) { mActiveKey = key; });
|
||||
row->button->on_pressed([this, key = item.key] {
|
||||
mActiveKey = key;
|
||||
if (mProps.onPressed) {
|
||||
mProps.onPressed(key);
|
||||
}
|
||||
});
|
||||
return row;
|
||||
}
|
||||
|
||||
void List::apply_items(std::vector<Item> items, const std::optional<SnapshotFocus>& snapshotFocus) {
|
||||
const SnapshotFocus focusState = snapshotFocus ? *snapshotFocus : capture_snapshot_focus();
|
||||
|
||||
std::unordered_map<uint64_t, std::unique_ptr<Row>> oldRows;
|
||||
oldRows.reserve(mRows.size());
|
||||
for (auto& row : mRows) {
|
||||
oldRows.emplace(row->key, std::move(row));
|
||||
}
|
||||
mRows.clear();
|
||||
mRows.reserve(items.size());
|
||||
|
||||
for (const auto& item : items) {
|
||||
if (auto node = oldRows.extract(item.key); !node.empty()) {
|
||||
auto row = std::move(node.mapped());
|
||||
row->button->set_text(item.label);
|
||||
mRows.push_back(std::move(row));
|
||||
} else {
|
||||
mRows.push_back(create_row(item));
|
||||
}
|
||||
}
|
||||
|
||||
for (auto& row : oldRows | std::views::values) {
|
||||
mContent->RemoveChild(row->button->root());
|
||||
}
|
||||
|
||||
for (int i = 0; i < static_cast<int>(mRows.size()); ++i) {
|
||||
auto* desired = mRows[i]->button->root();
|
||||
auto* current = mContent->GetChild(i);
|
||||
if (current != desired) {
|
||||
auto element = mContent->RemoveChild(desired);
|
||||
mContent->InsertBefore(std::move(element), current);
|
||||
}
|
||||
}
|
||||
|
||||
mRowsByKey.clear();
|
||||
mRowsByKey.reserve(mRows.size());
|
||||
for (const auto& row : mRows) {
|
||||
mRowsByKey.emplace(row->key, row.get());
|
||||
}
|
||||
mItems = std::move(items);
|
||||
|
||||
if (mRows.empty()) {
|
||||
mViewport->SetProperty("display", "none");
|
||||
mEmpty->RemoveProperty("display");
|
||||
} else {
|
||||
mViewport->RemoveProperty("display");
|
||||
mEmpty->SetProperty("display", "none");
|
||||
}
|
||||
|
||||
if (focusState.owned && !mRows.empty()) {
|
||||
uint64_t targetKey =
|
||||
mRows[std::min(focusState.index, static_cast<int>(mRows.size()) - 1)]->key;
|
||||
if (focusState.key && mRowsByKey.contains(*focusState.key)) {
|
||||
targetKey = *focusState.key;
|
||||
}
|
||||
request_focus(targetKey, false);
|
||||
} else {
|
||||
mPendingFocusKey.reset();
|
||||
mPendingFocusFrames = 0;
|
||||
mPendingFocusMayEnterList = false;
|
||||
}
|
||||
|
||||
mCullDirty = true;
|
||||
mLayoutScanFrames = 2;
|
||||
}
|
||||
|
||||
void List::update_culling() {
|
||||
const float viewTop = mViewport->GetAbsoluteOffset(Rml::BoxArea::Border).y;
|
||||
const float viewHeight = mViewport->GetClientHeight();
|
||||
if (viewHeight <= 0.0f) {
|
||||
return;
|
||||
}
|
||||
auto* context = mRoot->GetContext();
|
||||
const Row* focusedRow = context != nullptr ? row_from_element(context->GetFocusElement()) : nullptr;
|
||||
|
||||
for (const auto& row : mRows) {
|
||||
auto* element = row->button->root();
|
||||
const float top = element->GetAbsoluteOffset(Rml::BoxArea::Border).y - viewTop;
|
||||
const bool inWindow =
|
||||
top + element->GetOffsetHeight() >= -viewHeight && top <= viewHeight * 2.0f;
|
||||
const bool focusGuard =
|
||||
row.get() == focusedRow || (mPendingFocusKey && row->key == *mPendingFocusKey);
|
||||
const bool shouldShow = inWindow || focusGuard;
|
||||
if (shouldShow && row->culled) {
|
||||
show_row(*row);
|
||||
} else if (!shouldShow && !row->culled) {
|
||||
row->culled = true;
|
||||
element->SetProperty("visibility", "hidden");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void List::show_row(Row& row) {
|
||||
if (!row.culled) {
|
||||
return;
|
||||
}
|
||||
row.button->update();
|
||||
row.button->root()->RemoveProperty("visibility");
|
||||
row.culled = false;
|
||||
}
|
||||
|
||||
bool List::focus_row(int index, bool mayEnterList) {
|
||||
if (index < 0 || index >= static_cast<int>(mRows.size())) {
|
||||
return false;
|
||||
}
|
||||
auto& row = *mRows[index];
|
||||
show_row(row);
|
||||
row.button->update();
|
||||
if (row.button->root()->IsPseudoClassSet("disabled")) {
|
||||
return false;
|
||||
}
|
||||
if (row.button->focus()) {
|
||||
mActiveKey = row.key;
|
||||
mPendingFocusKey.reset();
|
||||
mPendingFocusFrames = 0;
|
||||
mPendingFocusMayEnterList = false;
|
||||
return true;
|
||||
}
|
||||
request_focus(row.key, mayEnterList);
|
||||
return true;
|
||||
}
|
||||
|
||||
void List::request_focus(uint64_t key, bool mayEnterList) {
|
||||
const auto it = mRowsByKey.find(key);
|
||||
if (it == mRowsByKey.end()) {
|
||||
return;
|
||||
}
|
||||
show_row(*it->second);
|
||||
mPendingFocusKey = key;
|
||||
mPendingFocusFrames = 2;
|
||||
mPendingFocusMayEnterList = mayEnterList;
|
||||
}
|
||||
|
||||
void List::update_pending_focus() {
|
||||
if (!mPendingFocusKey) {
|
||||
return;
|
||||
}
|
||||
const auto it = mRowsByKey.find(*mPendingFocusKey);
|
||||
if (it == mRowsByKey.end()) {
|
||||
mPendingFocusKey.reset();
|
||||
return;
|
||||
}
|
||||
|
||||
auto* context = mRoot->GetContext();
|
||||
auto* focusedElement = context != nullptr ? context->GetFocusElement() : nullptr;
|
||||
if (!mPendingFocusMayEnterList && focusedElement != nullptr && !contains(focusedElement)) {
|
||||
mPendingFocusKey.reset();
|
||||
return;
|
||||
}
|
||||
if (it->second->button->contains(focusedElement)) {
|
||||
mPendingFocusKey.reset();
|
||||
mPendingFocusFrames = 0;
|
||||
mPendingFocusMayEnterList = false;
|
||||
return;
|
||||
}
|
||||
if (mPendingFocusFrames > 0) {
|
||||
--mPendingFocusFrames;
|
||||
return;
|
||||
}
|
||||
|
||||
auto& row = *it->second;
|
||||
show_row(row);
|
||||
row.button->update();
|
||||
if (!row.button->root()->IsPseudoClassSet("disabled") && row.button->focus()) {
|
||||
mActiveKey = row.key;
|
||||
}
|
||||
mPendingFocusKey.reset();
|
||||
mPendingFocusMayEnterList = false;
|
||||
}
|
||||
|
||||
void List::handle_keydown(Rml::Event& event) {
|
||||
const NavCommand command = map_nav_event(event);
|
||||
if (command != NavCommand::Down && command != NavCommand::Up) {
|
||||
return;
|
||||
}
|
||||
Row* focusedRow = row_from_element(event.GetTargetElement());
|
||||
if (focusedRow == nullptr) {
|
||||
return;
|
||||
}
|
||||
mActiveKey = focusedRow->key;
|
||||
const int direction = command == NavCommand::Down ? 1 : -1;
|
||||
for (int i = row_index(focusedRow->key) + direction;
|
||||
i >= 0 && i < static_cast<int>(mRows.size()); i += direction)
|
||||
{
|
||||
if (focus_row(i, true)) {
|
||||
mDoAud_seStartMenu(kSoundItemFocus);
|
||||
event.StopPropagation();
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
} // namespace dusk::ui
|
||||
@@ -0,0 +1,80 @@
|
||||
#pragma once
|
||||
|
||||
#include "button.hpp"
|
||||
|
||||
#include <cstdint>
|
||||
#include <functional>
|
||||
#include <memory>
|
||||
#include <optional>
|
||||
#include <unordered_map>
|
||||
#include <vector>
|
||||
|
||||
namespace dusk::ui {
|
||||
|
||||
class List : public FluentComponent<List> {
|
||||
public:
|
||||
struct Item {
|
||||
uint64_t key = 0;
|
||||
Rml::String label;
|
||||
};
|
||||
|
||||
struct Props {
|
||||
std::vector<Item> items;
|
||||
std::function<void(uint64_t)> onPressed;
|
||||
std::function<bool(uint64_t)> isSelected;
|
||||
std::function<bool(uint64_t)> isDisabled;
|
||||
};
|
||||
|
||||
List(Rml::Element* parent, Props props);
|
||||
|
||||
void set_items(std::vector<Item> items);
|
||||
void update() override;
|
||||
bool focus() override;
|
||||
bool focus_from(NavCommand direction) override;
|
||||
|
||||
private:
|
||||
struct SnapshotFocus {
|
||||
bool owned = false;
|
||||
std::optional<uint64_t> key;
|
||||
int index = -1;
|
||||
};
|
||||
|
||||
struct Row {
|
||||
uint64_t key = 0;
|
||||
std::unique_ptr<ControlledButton> button;
|
||||
bool culled = true;
|
||||
};
|
||||
|
||||
Row* row_from_element(Rml::Element* element) const;
|
||||
int row_index(uint64_t key) const;
|
||||
SnapshotFocus capture_snapshot_focus();
|
||||
std::unique_ptr<Row> create_row(const Item& item);
|
||||
void apply_items(std::vector<Item> items, const std::optional<SnapshotFocus>& snapshotFocus = {});
|
||||
void update_culling();
|
||||
void show_row(Row& row);
|
||||
bool focus_row(int index, bool mayEnterList);
|
||||
void request_focus(uint64_t key, bool mayEnterList);
|
||||
void update_pending_focus();
|
||||
void handle_keydown(Rml::Event& event);
|
||||
|
||||
Props mProps;
|
||||
Rml::Element* mViewport = nullptr;
|
||||
Rml::Element* mContent = nullptr;
|
||||
Rml::Element* mEmpty = nullptr;
|
||||
std::vector<Item> mItems;
|
||||
std::optional<std::vector<Item>> mPendingItems;
|
||||
std::optional<SnapshotFocus> mPendingSnapshotFocus;
|
||||
std::vector<std::unique_ptr<Row>> mRows;
|
||||
std::unordered_map<uint64_t, Row*> mRowsByKey;
|
||||
std::optional<uint64_t> mActiveKey;
|
||||
std::optional<uint64_t> mPendingFocusKey;
|
||||
int mPendingFocusFrames = 0;
|
||||
bool mPendingFocusMayEnterList = false;
|
||||
bool mCullDirty = true;
|
||||
int mLayoutScanFrames = 2;
|
||||
float mLastScrollTop = -1.0f;
|
||||
float mLastViewportWidth = -1.0f;
|
||||
float mLastViewportHeight = -1.0f;
|
||||
};
|
||||
|
||||
} // namespace dusk::ui
|
||||
@@ -129,7 +129,6 @@ void LogsWindow::build_content(Rml::Element* content) {
|
||||
mScrollElem = pane.root();
|
||||
mLinesElem = append(pane.root(), "div");
|
||||
mLinesElem->SetClass("log-lines", true);
|
||||
pane.finalize();
|
||||
|
||||
listen(mScrollElem, Rml::EventId::Scroll, [this](Rml::Event&) {
|
||||
const float bottom = mScrollElem->GetScrollHeight() - mScrollElem->GetClientHeight();
|
||||
|
||||
@@ -69,6 +69,7 @@ Component* build_mod_control(Pane& pane, Pane* helpPane, ModControlSpec spec) {
|
||||
.isDisabled = s.isDisabled,
|
||||
.isModified = s.isModified,
|
||||
.maxLength = s.maxLength,
|
||||
.setOnChange = s.stringSetOnChange,
|
||||
});
|
||||
break;
|
||||
case ModControlSpec::Kind::Color:
|
||||
|
||||
@@ -39,6 +39,7 @@ struct ModControlSpec {
|
||||
Rml::String suffix;
|
||||
std::vector<Rml::String> options;
|
||||
int maxLength = -1;
|
||||
bool stringSetOnChange = false;
|
||||
std::vector<Rml::String> colorPresets;
|
||||
bool colorAlpha = false;
|
||||
};
|
||||
|
||||
@@ -192,8 +192,6 @@ void ModsWindow::build_content(Rml::Element* content) {
|
||||
|
||||
if (mods::ModLoader::instance().mods().empty()) {
|
||||
listPane.add_text("No mods installed.");
|
||||
listPane.finalize();
|
||||
detailPane.finalize();
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -214,8 +212,6 @@ void ModsWindow::build_content(Rml::Element* content) {
|
||||
}
|
||||
build_detail(detailPane, *mSelectedMod);
|
||||
mark_current_entry();
|
||||
|
||||
listPane.finalize();
|
||||
}
|
||||
|
||||
void ModsWindow::build_detail(Pane& pane, mods::LoadedMod& mod) {
|
||||
@@ -279,8 +275,6 @@ void ModsWindow::build_detail(Pane& pane, mods::LoadedMod& mod) {
|
||||
if (mod.active) {
|
||||
mods::svc::ui_build_mods_panels(mod, pane);
|
||||
}
|
||||
|
||||
pane.finalize();
|
||||
}
|
||||
|
||||
void ModsWindow::mark_current_entry() {
|
||||
|
||||
@@ -90,11 +90,6 @@ Pane::Pane(Rml::Element* parent, Type type) : FluentComponent(createRoot(parent)
|
||||
}
|
||||
}
|
||||
|
||||
void Pane::update() {
|
||||
finalize();
|
||||
Component::update();
|
||||
}
|
||||
|
||||
void Pane::set_selected_item(int index) {
|
||||
if (mType == Type::Uncontrolled) {
|
||||
return;
|
||||
@@ -191,22 +186,8 @@ Rml::Element* Pane::add_rml(const Rml::String& rml) {
|
||||
return elem;
|
||||
}
|
||||
|
||||
void Pane::finalize() {
|
||||
if (finalized) {
|
||||
return;
|
||||
}
|
||||
finalized = true;
|
||||
|
||||
// Append spacer element to the bottom. RmlUi does not properly handle
|
||||
// padding-bottom or margin-bottom on a scrollable flex container, so
|
||||
// we need to create a fake spacer with an actual layout height to get
|
||||
// padding at the bottom of a scrollable container.
|
||||
append(mRoot, "spacer");
|
||||
}
|
||||
|
||||
void Pane::clear() {
|
||||
clear_children();
|
||||
finalized = false;
|
||||
}
|
||||
|
||||
} // namespace dusk::ui
|
||||
|
||||
@@ -18,7 +18,6 @@ public:
|
||||
|
||||
bool focus() override;
|
||||
bool focus_last();
|
||||
void update() override;
|
||||
|
||||
void set_selected_item(int index);
|
||||
Component& register_control(
|
||||
@@ -37,12 +36,10 @@ public:
|
||||
}
|
||||
Rml::Element* add_text(const Rml::String& text);
|
||||
Rml::Element* add_rml(const Rml::String& rml);
|
||||
void finalize();
|
||||
void clear();
|
||||
|
||||
private:
|
||||
Type mType;
|
||||
bool finalized = false;
|
||||
};
|
||||
|
||||
} // namespace dusk::ui
|
||||
|
||||
@@ -6,8 +6,8 @@ namespace dusk::ui {
|
||||
|
||||
BaseStringButton::BaseStringButton(Rml::Element* parent, Props props)
|
||||
: BaseControlledSelectButton(parent, {std::move(props.key)}), mType(std::move(props.type)),
|
||||
mMaxLength(props.maxLength) {
|
||||
mInputListeners.reserve(4);
|
||||
mMaxLength(props.maxLength), mSetOnChange(props.setOnChange) {
|
||||
mInputListeners.reserve(5);
|
||||
}
|
||||
|
||||
void BaseStringButton::update() {
|
||||
@@ -35,8 +35,9 @@ void BaseStringButton::start_editing() {
|
||||
if (mInputElem == nullptr) {
|
||||
return;
|
||||
}
|
||||
mOriginalValue = input_value();
|
||||
mInputElem->SetAttribute("type", mType);
|
||||
mInputElem->SetAttribute("value", input_value());
|
||||
mInputElem->SetAttribute("value", mOriginalValue);
|
||||
if (mMaxLength > -1) {
|
||||
mInputElem->SetAttribute("maxlength", mMaxLength);
|
||||
}
|
||||
@@ -58,7 +59,10 @@ void BaseStringButton::start_editing() {
|
||||
mInputElem, Rml::EventId::Textinput, [this](Rml::Event& event) {
|
||||
if (event.GetTargetElement() == mInputElem) {
|
||||
const Rml::String text = event.GetParameter("text", Rml::String{});
|
||||
if (!text.empty() && std::ranges::all_of(text, [](const char c) { return c == '\r' || c == '\n' || c == '\t'; })) {
|
||||
if (!text.empty() &&
|
||||
std::ranges::all_of(
|
||||
text, [](const char c) { return c == '\r' || c == '\n' || c == '\t'; }))
|
||||
{
|
||||
event.StopImmediatePropagation();
|
||||
}
|
||||
}
|
||||
@@ -78,6 +82,14 @@ void BaseStringButton::start_editing() {
|
||||
mInputElem, Rml::EventId::Click, [](Rml::Event& event) { event.StopPropagation(); }));
|
||||
mInputListeners.emplace_back(std::make_unique<ScopedEventListener>(mInputElem,
|
||||
Rml::EventId::Blur, [this](Rml::Event&) { request_stop_editing(true, false); }));
|
||||
if (mSetOnChange) {
|
||||
mInputListeners.emplace_back(std::make_unique<ScopedEventListener>(
|
||||
mInputElem, Rml::EventId::Change, [this](Rml::Event& event) {
|
||||
if (event.GetTargetElement() == mInputElem) {
|
||||
set_value(mInputElem->GetValue());
|
||||
}
|
||||
}));
|
||||
}
|
||||
}
|
||||
|
||||
void BaseStringButton::request_stop_editing(bool commit, bool refocusRoot) {
|
||||
@@ -123,12 +135,15 @@ void BaseStringButton::stop_editing(bool commit, bool refocusRoot) {
|
||||
if (!is_editing()) {
|
||||
return;
|
||||
}
|
||||
if (commit) {
|
||||
if (!mSetOnChange && commit) {
|
||||
set_value(mInputElem->GetValue());
|
||||
} else if (mSetOnChange && !commit) {
|
||||
set_value(mOriginalValue);
|
||||
}
|
||||
mInputListeners.clear();
|
||||
mRoot->RemoveChild(mInputElem);
|
||||
mInputElem = nullptr;
|
||||
mOriginalValue.clear();
|
||||
|
||||
// Restore value element
|
||||
mValueElem->SetProperty(Rml::PropertyId::Visibility, Rml::Style::Visibility::Visible);
|
||||
@@ -140,7 +155,12 @@ void BaseStringButton::stop_editing(bool commit, bool refocusRoot) {
|
||||
}
|
||||
|
||||
StringButton::StringButton(Rml::Element* parent, Props props)
|
||||
: BaseStringButton(parent, {.key = std::move(props.key), .maxLength = props.maxLength}),
|
||||
: BaseStringButton(parent,
|
||||
{
|
||||
.key = std::move(props.key),
|
||||
.maxLength = props.maxLength,
|
||||
.setOnChange = props.setOnChange,
|
||||
}),
|
||||
mGetValue(std::move(props.getValue)), mSetValue(std::move(props.setValue)),
|
||||
mIsDisabled(std::move(props.isDisabled)), mIsModified(std::move(props.isModified)) {}
|
||||
|
||||
|
||||
@@ -12,6 +12,7 @@ public:
|
||||
Rml::String key;
|
||||
Rml::String type = "text";
|
||||
int maxLength = -1;
|
||||
bool setOnChange = false;
|
||||
};
|
||||
|
||||
BaseStringButton(Rml::Element* parent, Props props);
|
||||
@@ -30,13 +31,15 @@ private:
|
||||
void stop_editing(bool commit = true, bool refocusRoot = false);
|
||||
|
||||
Rml::ElementFormControlInput* mInputElem = nullptr;
|
||||
std::vector<std::unique_ptr<ScopedEventListener> > mInputListeners;
|
||||
std::vector<std::unique_ptr<ScopedEventListener>> mInputListeners;
|
||||
Rml::String mType;
|
||||
int mMaxLength;
|
||||
int mPendingInputFocusFrames = 0;
|
||||
bool mPendingStopEditing = false;
|
||||
bool mPendingCommit = true;
|
||||
bool mPendingRefocusRoot = false;
|
||||
bool mSetOnChange = false;
|
||||
Rml::String mOriginalValue;
|
||||
};
|
||||
|
||||
class StringButton : public BaseStringButton {
|
||||
@@ -48,6 +51,7 @@ public:
|
||||
std::function<bool()> isDisabled;
|
||||
std::function<bool()> isModified;
|
||||
int maxLength = -1;
|
||||
bool setOnChange = false;
|
||||
};
|
||||
|
||||
StringButton(Rml::Element* parent, Props props);
|
||||
|
||||
Reference in New Issue
Block a user