/* Copyright (C) 2025 OnionHEN / LightningMods * * Fluent builder for PS5 Debug Settings (system_settings) XML. * Business code describes widgets; build() returns the full document string. */ #pragma once #include #include #include #include #include #include namespace ps5ui { enum class Style { None, Center, Left }; /** * Escape for filesystem path attrs (icon): & < > " and / → //. * ShellUI resolves icon paths only when slashes are doubled. */ std::string escape(std::string_view text); /** * Escape for display attrs (title/second_title/description): & < > " only. * Do not double '/' — UI shows // literally in path text. */ std::string escape_xml(std::string_view text); struct Attrs { std::string id; std::string title; std::optional second_title; std::optional description; std::optional icon; std::optional file; std::optional value; std::optional confirm; std::optional confirm_phrase; std::optional keyboard_type; std::optional min_length; std::optional max_length; std::optional key; std::optional initial_focus_to; std::optional list_size; std::optional list_highlight; std::optional raw_title; std::optional restorable; Style style = Style::None; bool operator==(const Attrs &) const = default; }; struct Node { enum class Kind { SettingList, Toggle, Button, Label, Link, List, ListItem, TextField, UserCustom, Option, OptionItem, } kind = Kind::SettingList; Attrs attrs; std::vector children; bool operator==(const Node &) const = default; }; const char *node_tag(Node::Kind kind); std::vector> node_attributes(const Node &node); std::string build_document(const Node &root, std::string_view plugin = "debug_settings_plugin"); class ListBuilder { public: ListBuilder& item(std::string id, std::string title, std::string value, std::optional icon = std::nullopt); private: template friend class GroupT; explicit ListBuilder(Node& list_node) : list_node_(list_node) {} Node& list_node_; }; class Group; // nested scopes always use Group /** * CRTP base so Page::toggle(...).build() type-checks (returns Derived&). */ template class GroupT { public: Derived& label(std::string id, std::string title, Style style = Style::None, std::optional icon = std::nullopt); Derived& button(std::string id, std::string title, std::optional second_title = std::nullopt, std::optional description = std::nullopt, std::optional icon = std::nullopt, Style style = Style::None, std::optional confirm = std::nullopt, std::optional confirm_phrase = std::nullopt); Derived& toggle(std::string id, std::string title, bool on, std::optional second_title = std::nullopt, std::optional description = std::nullopt, std::optional icon = std::nullopt, std::optional confirm = std::nullopt); Derived& link(std::string id, std::string title, std::string file, std::optional second_title = std::nullopt, std::optional icon = std::nullopt, std::optional confirm = std::nullopt, std::optional confirm_phrase = std::nullopt); Derived& list(std::string id, std::string title, const std::function& body, std::optional second_title = std::nullopt, std::optional value = std::nullopt, std::optional confirm = std::nullopt, std::optional confirm_phrase = std::nullopt, std::optional icon = std::nullopt); Derived& text_field(std::string id, std::string title, std::optional second_title = std::nullopt, std::optional keyboard_type = std::nullopt, std::optional min_length = std::nullopt, std::optional max_length = std::nullopt, std::optional key = std::nullopt, std::optional confirm = std::nullopt, std::optional confirm_phrase = std::nullopt, std::optional value = std::nullopt, std::optional icon = std::nullopt); /** Legacy Settings placeholder whose real Panel is appended during Reset. */ Derived& user_custom(std::string id); /** Options-button context menu container (page level). */ Derived& option(std::string id, const std::function& body, std::optional list_highlight = std::nullopt); /** Options-button context menu entry. */ Derived& option_item(std::string id, std::string title, std::optional raw_title = std::nullopt); Derived& group(std::string id, std::string title, const std::function& body, std::optional second_title = std::nullopt, std::optional icon = std::nullopt, std::optional initial_focus_to = std::nullopt, Style style = Style::None); Derived& add(Node node); protected: GroupT() = default; void bind(Node* node) { node_ = node; } Node* node_ = nullptr; Derived& self() { return static_cast(*this); } }; /** Nested setting_list scope (and the type accepted by group() callbacks). */ class Group : public GroupT { public: explicit Group(Node* node) { bind(node); } }; /** * Root document: one root setting_list under system_settings. * Call build() to get the full XML string. */ class Page : public GroupT { public: Page(std::string root_list_id, std::string root_title, std::string plugin = "debug_settings_plugin"); Page& root_style(Style style); Page& root_focus(std::string id); Page& root_icon(std::string icon); Page& root_list_size(std::string size); Page& root_restorable(bool restorable); /** Serialize to a complete system_settings document. */ [[nodiscard]] std::string build() const; const Node &root() const { return root_; } private: std::string plugin_; Node root_; }; // ---- template method definitions ---- template Derived& GroupT::label(std::string id, std::string title, Style style, std::optional icon) { Node n; n.kind = Node::Kind::Label; n.attrs.id = std::move(id); n.attrs.title = std::move(title); n.attrs.style = style; n.attrs.icon = std::move(icon); return add(std::move(n)); } template Derived& GroupT::button(std::string id, std::string title, std::optional second_title, std::optional description, std::optional icon, Style style, std::optional confirm, std::optional confirm_phrase) { Node n; n.kind = Node::Kind::Button; n.attrs.id = std::move(id); n.attrs.title = std::move(title); n.attrs.second_title = std::move(second_title); n.attrs.description = std::move(description); n.attrs.icon = std::move(icon); n.attrs.style = style; n.attrs.confirm = std::move(confirm); n.attrs.confirm_phrase = std::move(confirm_phrase); return add(std::move(n)); } template Derived& GroupT::toggle(std::string id, std::string title, bool on, std::optional second_title, std::optional description, std::optional icon, std::optional confirm) { Node n; n.kind = Node::Kind::Toggle; n.attrs.id = std::move(id); n.attrs.title = std::move(title); n.attrs.second_title = std::move(second_title); n.attrs.description = std::move(description); n.attrs.icon = std::move(icon); n.attrs.confirm = std::move(confirm); n.attrs.value = on ? "1" : "0"; return add(std::move(n)); } template Derived& GroupT::link(std::string id, std::string title, std::string file, std::optional second_title, std::optional icon, std::optional confirm, std::optional confirm_phrase) { Node n; n.kind = Node::Kind::Link; n.attrs.id = std::move(id); n.attrs.title = std::move(title); n.attrs.file = std::move(file); n.attrs.second_title = std::move(second_title); n.attrs.icon = std::move(icon); n.attrs.confirm = std::move(confirm); n.attrs.confirm_phrase = std::move(confirm_phrase); return add(std::move(n)); } template Derived& GroupT::list(std::string id, std::string title, const std::function& body, std::optional second_title, std::optional value, std::optional confirm, std::optional confirm_phrase, std::optional icon) { Node n; n.kind = Node::Kind::List; n.attrs.id = std::move(id); n.attrs.title = std::move(title); n.attrs.second_title = std::move(second_title); n.attrs.value = std::move(value); n.attrs.confirm = std::move(confirm); n.attrs.confirm_phrase = std::move(confirm_phrase); n.attrs.icon = std::move(icon); node_->children.push_back(std::move(n)); ListBuilder lb(node_->children.back()); if (body) body(lb); return self(); } template Derived& GroupT::text_field(std::string id, std::string title, std::optional second_title, std::optional keyboard_type, std::optional min_length, std::optional max_length, std::optional key, std::optional confirm, std::optional confirm_phrase, std::optional value, std::optional icon) { Node n; n.kind = Node::Kind::TextField; n.attrs.id = std::move(id); n.attrs.title = std::move(title); n.attrs.second_title = std::move(second_title); n.attrs.keyboard_type = std::move(keyboard_type); n.attrs.min_length = std::move(min_length); n.attrs.max_length = std::move(max_length); n.attrs.key = std::move(key); n.attrs.confirm = std::move(confirm); n.attrs.confirm_phrase = std::move(confirm_phrase); n.attrs.value = std::move(value); n.attrs.icon = std::move(icon); return add(std::move(n)); } template Derived& GroupT::user_custom(std::string id) { Node n; n.kind = Node::Kind::UserCustom; n.attrs.id = std::move(id); return add(std::move(n)); } template Derived& GroupT::option(std::string id, const std::function& body, std::optional list_highlight) { Node n; n.kind = Node::Kind::Option; n.attrs.id = std::move(id); n.attrs.list_highlight = std::move(list_highlight); node_->children.push_back(std::move(n)); Group sub(&node_->children.back()); if (body) body(sub); return self(); } template Derived& GroupT::option_item(std::string id, std::string title, std::optional raw_title) { Node n; n.kind = Node::Kind::OptionItem; n.attrs.id = std::move(id); n.attrs.title = std::move(title); n.attrs.raw_title = std::move(raw_title); return add(std::move(n)); } template Derived& GroupT::group(std::string id, std::string title, const std::function& body, std::optional second_title, std::optional icon, std::optional initial_focus_to, Style style) { Node n; n.kind = Node::Kind::SettingList; n.attrs.id = std::move(id); n.attrs.title = std::move(title); n.attrs.second_title = std::move(second_title); n.attrs.icon = std::move(icon); n.attrs.initial_focus_to = std::move(initial_focus_to); n.attrs.style = style; node_->children.push_back(std::move(n)); Group sub(&node_->children.back()); if (body) body(sub); return self(); } template Derived& GroupT::add(Node node) { node_->children.push_back(std::move(node)); return self(); } } // namespace ps5ui