Pulp 1.88.0
Cross-platform audio plugin framework
Loading...
Searching...
No Matches
property_panel.hpp
Go to the documentation of this file.
1#pragma once
2
39
40#include <algorithm>
41#include <cstdint>
42#include <functional>
43#include <optional>
44#include <string>
45#include <string_view>
46#include <unordered_set>
47#include <utility>
48#include <variant>
49#include <vector>
50
52#include <pulp/view/view.hpp>
53
54namespace pulp::view {
55
56class PropertyPanel : public View {
57public:
66
67 using Value = std::variant<bool,
68 int,
69 float,
70 std::vector<int>,
71 std::string,
72 std::monostate>;
73
74 struct Property {
75 std::string key;
76 std::string label;
79 // Slider-only constraints.
80 float min = 0.0f;
81 float max = 1.0f;
82 // Choice / MultiChoice options.
83 std::vector<std::string> options;
84 // Per-property change observer fires AFTER the value updates +
85 // BEFORE persistence is written.
86 std::function<void(const Value&)> on_change;
87 // Button-only action; fires on `click`.
88 std::function<void()> on_click;
89 };
90
91 PropertyPanel() = default;
92
93 // ── Builders (preferred entry points) ────────────────────────────
94
95 void add_boolean(std::string key, std::string label, bool initial,
96 std::function<void(bool)> on_change = {}) {
97 Property p;
99 p.key = std::move(key);
100 p.label = std::move(label);
101 p.value = initial;
102 if (on_change) {
103 p.on_change = [cb = std::move(on_change)](const Value& v) {
104 if (auto* b = std::get_if<bool>(&v)) cb(*b);
105 };
106 }
107 register_property(std::move(p));
108 }
109
110 void add_choice(std::string key, std::string label,
111 std::vector<std::string> options, int initial_index,
112 std::function<void(int)> on_change = {}) {
113 Property p;
114 p.kind = Kind::Choice;
115 p.key = std::move(key);
116 p.label = std::move(label);
117 p.options = std::move(options);
118 // Clamp the initial index defensively so the panel never holds
119 // an out-of-range value.
120 if (p.options.empty()) {
121 p.value = 0;
122 } else {
123 if (initial_index < 0) initial_index = 0;
124 if (initial_index >= static_cast<int>(p.options.size())) {
125 initial_index = static_cast<int>(p.options.size()) - 1;
126 }
127 p.value = initial_index;
128 }
129 if (on_change) {
130 p.on_change = [cb = std::move(on_change)](const Value& v) {
131 if (auto* i = std::get_if<int>(&v)) cb(*i);
132 };
133 }
134 register_property(std::move(p));
135 }
136
137 void add_slider(std::string key, std::string label, float min, float max,
138 float initial, std::function<void(float)> on_change = {}) {
139 Property p;
140 p.kind = Kind::Slider;
141 p.key = std::move(key);
142 p.label = std::move(label);
143 p.min = min;
144 p.max = max;
145 p.value = clamp(initial, min, max);
146 if (on_change) {
147 p.on_change = [cb = std::move(on_change)](const Value& v) {
148 if (auto* f = std::get_if<float>(&v)) cb(*f);
149 };
150 }
151 register_property(std::move(p));
152 }
153
154 void add_button(std::string key, std::string label,
155 std::function<void()> on_click) {
156 Property p;
158 p.key = std::move(key);
159 p.label = std::move(label);
160 p.value = std::monostate{};
161 p.on_click = std::move(on_click);
162 register_property(std::move(p));
163 }
164
165 void add_multi_choice(std::string key, std::string label,
166 std::vector<std::string> options,
167 std::vector<int> initial_selection,
168 std::function<void(const std::vector<int>&)> on_change = {}) {
169 Property p;
170 p.kind = Kind::MultiChoice;
171 p.key = std::move(key);
172 p.label = std::move(label);
173 p.options = std::move(options);
174 normalize_indices(initial_selection, p.options.size());
175 p.value = std::move(initial_selection);
176 if (on_change) {
177 p.on_change = [cb = std::move(on_change)](const Value& v) {
178 if (auto* xs = std::get_if<std::vector<int>>(&v)) cb(*xs);
179 };
180 }
181 register_property(std::move(p));
182 }
183
184 void add_text(std::string key, std::string label, std::string initial,
185 std::function<void(const std::string&)> on_change = {}) {
186 Property p;
187 p.kind = Kind::Text;
188 p.key = std::move(key);
189 p.label = std::move(label);
190 p.value = std::move(initial);
191 if (on_change) {
192 p.on_change = [cb = std::move(on_change)](const Value& v) {
193 if (auto* s = std::get_if<std::string>(&v)) cb(*s);
194 };
195 }
196 register_property(std::move(p));
197 }
198
199 // ── Read access ──────────────────────────────────────────────────
200
201 std::size_t size() const { return properties_.size(); }
202 bool empty() const { return properties_.empty(); }
203
204 const std::vector<Property>& properties() const { return properties_; }
205
206 const Property* find(std::string_view key) const {
207 for (const auto& p : properties_) {
208 if (p.key == key) return &p;
209 }
210 return nullptr;
211 }
212
213 std::optional<bool> get_bool(std::string_view key) const {
214 if (const auto* p = find(key))
215 if (const auto* b = std::get_if<bool>(&p->value)) return *b;
216 return std::nullopt;
217 }
218 std::optional<int> get_choice(std::string_view key) const {
219 if (const auto* p = find(key))
220 if (const auto* i = std::get_if<int>(&p->value)) return *i;
221 return std::nullopt;
222 }
223 std::optional<float> get_slider(std::string_view key) const {
224 if (const auto* p = find(key))
225 if (const auto* f = std::get_if<float>(&p->value)) return *f;
226 return std::nullopt;
227 }
228 std::optional<std::vector<int>> get_multi_choice(std::string_view key) const {
229 if (const auto* p = find(key))
230 if (const auto* xs = std::get_if<std::vector<int>>(&p->value)) return *xs;
231 return std::nullopt;
232 }
233 std::optional<std::string> get_text(std::string_view key) const {
234 if (const auto* p = find(key))
235 if (const auto* s = std::get_if<std::string>(&p->value)) return *s;
236 return std::nullopt;
237 }
238
239 // ── Mutation (drives observers + persistence) ────────────────────
240
243 bool set_bool(std::string_view key, bool v) {
244 auto* p = find_mut(key);
245 if (!p || p->kind != Kind::BooleanToggle) return false;
246 p->value = v;
247 fire(*p);
248 return true;
249 }
250 bool set_choice(std::string_view key, int index) {
251 auto* p = find_mut(key);
252 if (!p || p->kind != Kind::Choice) return false;
253 if (p->options.empty()) {
254 p->value = 0;
255 } else {
256 if (index < 0) index = 0;
257 if (index >= static_cast<int>(p->options.size())) {
258 index = static_cast<int>(p->options.size()) - 1;
259 }
260 p->value = index;
261 }
262 fire(*p);
263 return true;
264 }
265 bool set_slider(std::string_view key, float v) {
266 auto* p = find_mut(key);
267 if (!p || p->kind != Kind::Slider) return false;
268 p->value = clamp(v, p->min, p->max);
269 fire(*p);
270 return true;
271 }
272 bool set_multi_choice(std::string_view key, std::vector<int> selection) {
273 auto* p = find_mut(key);
274 if (!p || p->kind != Kind::MultiChoice) return false;
275 normalize_indices(selection, p->options.size());
276 p->value = std::move(selection);
277 fire(*p);
278 return true;
279 }
280 bool set_text(std::string_view key, std::string v) {
281 auto* p = find_mut(key);
282 if (!p || p->kind != Kind::Text) return false;
283 p->value = std::move(v);
284 fire(*p);
285 return true;
286 }
287
290 bool click(std::string_view key) {
291 auto* p = find_mut(key);
292 if (!p || p->kind != Kind::Button) return false;
293 if (p->on_click) p->on_click();
294 return true;
295 }
296
297 // ── Simulation aliases (named for headless tests) ───────────────
298
299 bool simulate_toggle(std::string_view key) {
300 auto cur = get_bool(key);
301 if (!cur) return false;
302 return set_bool(key, !*cur);
303 }
304 bool simulate_choose(std::string_view key, int index) {
305 return set_choice(key, index);
306 }
307 bool simulate_slide(std::string_view key, float v) {
308 return set_slider(key, v);
309 }
310 bool simulate_click(std::string_view key) { return click(key); }
311 bool simulate_multi_check(std::string_view key, int index, bool on) {
312 auto cur = get_multi_choice(key);
313 if (!cur) return false;
314 std::vector<int> next = *cur;
315 if (on) {
316 // Insert sorted-unique.
317 if (std::find(next.begin(), next.end(), index) == next.end()) {
318 next.push_back(index);
319 }
320 } else {
321 next.erase(std::remove(next.begin(), next.end(), index), next.end());
322 }
323 return set_multi_choice(key, std::move(next));
324 }
325 bool simulate_set_text(std::string_view key, std::string v) {
326 return set_text(key, std::move(v));
327 }
328
329 // ── Persistence ──────────────────────────────────────────────────
330
335 props_ = props;
336 if (!props_) return;
337 for (auto& p : properties_) {
338 load_from_props(p);
339 }
340 }
341 bool has_persistence() const { return props_ != nullptr; }
342
343private:
344 void register_property(Property p) {
345 // Deduplicate by key: re-registering the same key replaces the
346 // old slot in place (callers might rebuild a panel on theme
347 // switch, etc.).
348 for (auto& existing : properties_) {
349 if (existing.key == p.key) {
350 existing = std::move(p);
351 if (props_) load_from_props(existing);
352 return;
353 }
354 }
355 properties_.push_back(std::move(p));
356 if (props_) load_from_props(properties_.back());
357 }
358
359 Property* find_mut(std::string_view key) {
360 for (auto& p : properties_) {
361 if (p.key == key) return &p;
362 }
363 return nullptr;
364 }
365
366 void fire(const Property& p) {
367 if (p.on_change) p.on_change(p.value);
368 persist(p);
369 }
370
371 void persist(const Property& p) const {
372 if (!props_) return;
373 switch (p.kind) {
375 if (auto* b = std::get_if<bool>(&p.value))
376 props_->set_bool(p.key, *b);
377 break;
378 case Kind::Choice:
379 if (auto* i = std::get_if<int>(&p.value))
380 props_->set_int(p.key, *i);
381 break;
382 case Kind::Slider:
383 if (auto* f = std::get_if<float>(&p.value))
384 props_->set_double(p.key, static_cast<double>(*f));
385 break;
387 if (auto* xs = std::get_if<std::vector<int>>(&p.value))
388 props_->set_string(p.key, encode_indices(*xs));
389 break;
390 case Kind::Text:
391 if (auto* s = std::get_if<std::string>(&p.value))
392 props_->set_string(p.key, *s);
393 break;
394 case Kind::Button:
395 break; // No persisted state.
396 }
397 }
398
399 void load_from_props(Property& p) {
400 if (!props_) return;
401 switch (p.kind) {
403 if (auto v = props_->get_bool(p.key)) p.value = *v;
404 break;
405 case Kind::Choice:
406 if (auto v = props_->get_int(p.key)) {
407 int i = static_cast<int>(*v);
408 if (!p.options.empty()) {
409 if (i < 0) i = 0;
410 if (i >= static_cast<int>(p.options.size())) {
411 i = static_cast<int>(p.options.size()) - 1;
412 }
413 } else {
414 i = 0;
415 }
416 p.value = i;
417 }
418 break;
419 case Kind::Slider:
420 if (auto v = props_->get_double(p.key)) {
421 p.value = clamp(static_cast<float>(*v), p.min, p.max);
422 }
423 break;
425 if (auto v = props_->get_string(p.key)) {
426 auto xs = decode_indices(*v);
427 normalize_indices(xs, p.options.size());
428 p.value = std::move(xs);
429 }
430 break;
431 case Kind::Text:
432 if (auto v = props_->get_string(p.key)) p.value = *v;
433 break;
434 case Kind::Button:
435 break;
436 }
437 }
438
439 static float clamp(float v, float lo, float hi) {
440 if (hi < lo) return v;
441 if (v < lo) return lo;
442 if (v > hi) return hi;
443 return v;
444 }
445 static void normalize_indices(std::vector<int>& xs, std::size_t option_count) {
446 std::unordered_set<int> seen;
447 std::vector<int> out;
448 out.reserve(xs.size());
449 for (int i : xs) {
450 if (option_count > 0
451 && (i < 0 || i >= static_cast<int>(option_count))) continue;
452 if (seen.insert(i).second) out.push_back(i);
453 }
454 std::sort(out.begin(), out.end());
455 xs.swap(out);
456 }
457 static std::string encode_indices(const std::vector<int>& xs) {
458 std::string out;
459 for (std::size_t i = 0; i < xs.size(); ++i) {
460 if (i) out.push_back(',');
461 out += std::to_string(xs[i]);
462 }
463 return out;
464 }
465 static std::vector<int> decode_indices(std::string_view s) {
466 std::vector<int> out;
467 std::size_t i = 0;
468 while (i < s.size()) {
469 std::size_t j = s.find(',', i);
470 if (j == std::string_view::npos) j = s.size();
471 auto tok = s.substr(i, j - i);
472 // Trim whitespace.
473 while (!tok.empty() && (tok.front() == ' ' || tok.front() == '\t'))
474 tok.remove_prefix(1);
475 while (!tok.empty() && (tok.back() == ' ' || tok.back() == '\t'))
476 tok.remove_suffix(1);
477 if (!tok.empty()) {
478 try {
479 out.push_back(std::stoi(std::string(tok)));
480 } catch (...) {
481 // Skip malformed entries — defensive against
482 // hand-edited properties files.
483 }
484 }
485 i = j + 1;
486 }
487 return out;
488 }
489
490 std::vector<Property> properties_;
491 pulp::state::PropertiesFile* props_ = nullptr;
492};
493
494} // namespace pulp::view
JSON-backed persistent settings file.
Definition properties_file.hpp:16
bool simulate_choose(std::string_view key, int index)
Definition property_panel.hpp:304
bool simulate_set_text(std::string_view key, std::string v)
Definition property_panel.hpp:325
std::optional< bool > get_bool(std::string_view key) const
Definition property_panel.hpp:213
bool click(std::string_view key)
Click a Button property.
Definition property_panel.hpp:290
void add_multi_choice(std::string key, std::string label, std::vector< std::string > options, std::vector< int > initial_selection, std::function< void(const std::vector< int > &)> on_change={})
Definition property_panel.hpp:165
const Property * find(std::string_view key) const
Definition property_panel.hpp:206
std::size_t size() const
Definition property_panel.hpp:201
bool set_text(std::string_view key, std::string v)
Definition property_panel.hpp:280
void add_slider(std::string key, std::string label, float min, float max, float initial, std::function< void(float)> on_change={})
Definition property_panel.hpp:137
bool set_bool(std::string_view key, bool v)
Set a Boolean property.
Definition property_panel.hpp:243
void add_choice(std::string key, std::string label, std::vector< std::string > options, int initial_index, std::function< void(int)> on_change={})
Definition property_panel.hpp:110
const std::vector< Property > & properties() const
Definition property_panel.hpp:204
void add_boolean(std::string key, std::string label, bool initial, std::function< void(bool)> on_change={})
Definition property_panel.hpp:95
bool empty() const
Definition property_panel.hpp:202
void add_text(std::string key, std::string label, std::string initial, std::function< void(const std::string &)> on_change={})
Definition property_panel.hpp:184
std::optional< float > get_slider(std::string_view key) const
Definition property_panel.hpp:223
void add_button(std::string key, std::string label, std::function< void()> on_click)
Definition property_panel.hpp:154
bool set_slider(std::string_view key, float v)
Definition property_panel.hpp:265
std::variant< bool, int, float, std::vector< int >, std::string, std::monostate > Value
Definition property_panel.hpp:67
bool set_multi_choice(std::string_view key, std::vector< int > selection)
Definition property_panel.hpp:272
std::optional< int > get_choice(std::string_view key) const
Definition property_panel.hpp:218
bool simulate_toggle(std::string_view key)
Definition property_panel.hpp:299
bool set_choice(std::string_view key, int index)
Definition property_panel.hpp:250
bool has_persistence() const
Definition property_panel.hpp:341
void bind_persistence(pulp::state::PropertiesFile *props)
Bind the panel to a PropertiesFile.
Definition property_panel.hpp:334
bool simulate_click(std::string_view key)
Definition property_panel.hpp:310
std::optional< std::vector< int > > get_multi_choice(std::string_view key) const
Definition property_panel.hpp:228
bool simulate_multi_check(std::string_view key, int index, bool on)
Definition property_panel.hpp:311
Kind
Definition property_panel.hpp:58
@ Slider
float in [min, max]
Definition property_panel.hpp:61
@ Choice
single int index into options
Definition property_panel.hpp:60
@ Button
action only (no persisted state)
Definition property_panel.hpp:62
@ Text
std::string value
Definition property_panel.hpp:64
@ BooleanToggle
bool — round-trips as 0/1
Definition property_panel.hpp:59
@ MultiChoice
ordered set of int indices into options
Definition property_panel.hpp:63
std::optional< std::string > get_text(std::string_view key) const
Definition property_panel.hpp:233
bool simulate_slide(std::string_view key, float v)
Definition property_panel.hpp:307
std::function< void()> on_click
Generic click callback (fires on mouse-down, if set).
Definition view.hpp:1476
@ label
Definition view.hpp:989
Hover-cursor tracking for platform window hosts.
Definition fwd.hpp:27
@ s
Definition input_events.hpp:41
@ j
Definition input_events.hpp:40
@ p
Definition input_events.hpp:40
@ i
Definition input_events.hpp:40
@ f
Definition input_events.hpp:39
@ b
Definition input_events.hpp:39
@ v
Definition input_events.hpp:41
@ on
Definition accessibility.hpp:132
Definition property_panel.hpp:74
std::string key
Persistence + lookup key.
Definition property_panel.hpp:75
Kind kind
Definition property_panel.hpp:77
Value value
Definition property_panel.hpp:78
std::string label
Display label.
Definition property_panel.hpp:76
float min
Definition property_panel.hpp:80
std::function< void(const Value &)> on_change
Definition property_panel.hpp:86
float max
Definition property_panel.hpp:81
std::vector< std::string > options
Definition property_panel.hpp:83
std::function< void()> on_click
Definition property_panel.hpp:88