Pulp 1.88.0
Cross-platform audio plugin framework
Loading...
Searching...
No Matches
tree_view.hpp
Go to the documentation of this file.
1#pragma once
2
5
6#include <pulp/view/view.hpp>
9#include <algorithm>
10#include <string>
11#include <vector>
12#include <memory>
13#include <functional>
14
15namespace pulp::view {
16
18struct TreeNode {
19 std::string label;
20 std::string icon;
21 bool expanded = false;
22 bool selectable = true;
23 void* user_data = nullptr;
24
25 std::vector<std::unique_ptr<TreeNode>> children;
26
28 TreeNode& add_child(std::string child_label) {
29 children.push_back(std::make_unique<TreeNode>());
30 children.back()->label = std::move(child_label);
31 return *children.back();
32 }
33
35 bool has_children() const { return !children.empty(); }
36
38 void toggle() { expanded = !expanded; }
39};
40
57class TreeView : public View {
58public:
66
68
70 TreeNode& root() { return root_; }
71 const TreeNode& root() const { return root_; }
72
75 void set_selection_style(SelectionStyle s) { selection_style_ = s; }
76 SelectionStyle selection_style() const { return selection_style_; }
77
79 std::function<void(TreeNode& node)> on_select;
80
82 std::function<void(TreeNode& node)> on_activate;
83
85 std::function<void(TreeNode& node, bool expanded)> on_toggle;
86
88 float row_height() const { return row_height_; }
89 void set_row_height(float h) { row_height_ = h; }
90
92 float indent() const { return indent_; }
93 void set_indent(float px) { indent_ = px; }
94
96 TreeNode* selected_node() const { return selected_; }
97
99 void set_selected_node(TreeNode* node) { selected_ = node; }
100
104 return find_by_user_data(root_, data);
105 }
106
110 int visible_node_count() const { return count_visible(root_, -1); }
111
116 float content_height() const {
117 return static_cast<float>(visible_node_count()) * row_height_;
118 }
119
121 float scroll_offset() const { return scroll_offset_; }
122
125 float max_scroll_offset() const {
126 return std::max(0.0f, content_height() - local_bounds().height);
127 }
128
132 void set_scroll_offset(float v) {
133 scroll_offset_ = std::clamp(v, 0.0f, max_scroll_offset());
134 }
135
136 void paint(canvas::Canvas& canvas) override {
137 auto b = local_bounds();
138
139 canvas.save();
140 canvas.clip_rect(b.x, b.y, b.width, b.height);
141
142 float y = b.y - scroll_offset_;
143 paint_node(canvas, root_, -1, b.x, y, b.width);
144
145 canvas.restore();
146 }
147
148 void on_mouse_event(const MouseEvent& event) override {
149 // Wheel/trackpad scroll, clamped so the top stays reachable.
150 if (event.is_wheel) {
151 set_scroll_offset(scroll_offset_ + event.scroll_delta_y);
153 return;
154 }
155 if (!event.is_down) return;
156
157 float y = event.position.y + scroll_offset_;
158 float current_y = 0;
159 int hit_depth = 0;
160 TreeNode* hit = find_node_at_y(root_, -1, current_y, y, &hit_depth);
161
162 if (hit) {
163 if (event.click_count == 2) {
164 if (on_activate) on_activate(*hit);
165 } else {
166 // Check if click is on the expand triangle area
167 float indent_x = static_cast<float>(hit_depth) * indent_;
168 float triangle_end = indent_x + indent_;
169 if (hit->has_children() && event.position.x < triangle_end) {
170 hit->toggle();
171 if (on_toggle) on_toggle(*hit, hit->expanded);
172 } else if (hit->selectable) {
173 selected_ = hit;
174 if (on_select) on_select(*hit);
175 }
176 }
177 }
178 }
179
180 bool on_key_event(const KeyEvent& event) override {
181 if (!event.is_down) return false;
182
183 if (event.key == KeyCode::right && selected_ && selected_->has_children()) {
184 selected_->expanded = true;
185 if (on_toggle) on_toggle(*selected_, true);
186 return true;
187 }
188 if (event.key == KeyCode::left && selected_) {
189 if (selected_->expanded && selected_->has_children()) {
190 selected_->expanded = false;
191 if (on_toggle) on_toggle(*selected_, false);
192 }
193 return true;
194 }
195 return false;
196 }
197
198 bool wants_mouse_input() const override { return true; }
199
200private:
201 TreeNode root_;
202 TreeNode* selected_ = nullptr;
203 SelectionStyle selection_style_ = SelectionStyle::standard;
204 float row_height_ = 22.0f;
205 float indent_ = 18.0f;
206 float scroll_offset_ = 0.0f;
207
212 static constexpr float kTriangleWidth = 18.0f;
215 static constexpr float kIconWidth = 18.0f;
218 static constexpr float kTriangleFontSize = 9.0f;
219
220 void paint_node(canvas::Canvas& canvas, TreeNode& node, int depth,
221 float x, float& y, float width) {
222 if (depth >= 0) { // Don't paint the invisible root
223 float indent_x = x + static_cast<float>(depth) * indent_;
224
225 const bool is_sel = (&node == selected_);
226 const bool accent = (selection_style_ == SelectionStyle::accent);
227
228 // Selection highlight. `accent` paints a translucent accent tint
229 // plus a teal left-edge bar (Ink & Signal); `standard` keeps the
230 // historic opaque fill.
231 if (is_sel) {
232 if (accent) {
233 canvas.set_fill_color(resolve_color("nav.selected.bg",
234 canvas::Color::rgba8(20, 184, 166, 38)));
235 canvas.fill_rect(x, y, width, row_height_);
236 canvas.set_fill_color(resolve_color("nav.selected.text",
237 canvas::Color::hex(0x14b8a6)));
238 canvas.fill_rect(x, y, 3.0f, row_height_);
239 } else {
240 canvas.set_fill_color(resolve_color("tree_selected_bg",
241 canvas::Color::hex(0x0f3460)));
242 canvas.fill_rect(x, y, width, row_height_);
243 }
244 }
245
246 // Row vertical midline — both the disclosure triangle and the
247 // label anchor here (GlyphCenter) so they line up exactly,
248 // instead of the triangle sitting below the label's baseline.
249 const float row_mid = y + row_height_ * 0.5f;
250
251 // Expand triangle. Draw with an explicit SMALL font so it
252 // doesn't inherit the stale/large font from a previous paint
253 // call (which made the glyph render oversized and overlap the
254 // label). The triangle sits in its own column, fully BEFORE
255 // the label text with a clear gap (see kTriangleWidth / the
256 // label_x offset below).
257 if (node.has_children()) {
258 canvas.set_font("system", kTriangleFontSize);
259 canvas.set_fill_color(resolve_color("text_muted",
260 canvas::Color::hex(0x808090)));
261 float tx = indent_x + 2;
263 const char* glyph = node.expanded
264 ? "\xe2\x96\xbc" // ▼ down-pointing
265 : "\xe2\x96\xb6"; // ▶ right-pointing
266 canvas.fill_text_anchored(glyph, tx, row_mid,
268 }
269
270 // Label — offset past the triangle column so the arrow always
271 // sits cleanly before the text with a visible gap, and anchored
272 // on the same row midline as the triangle.
273 float label_x = indent_x + kTriangleWidth;
274
275 // Optional leading icon (folder / file glyph). Drawn in its own
276 // column before the label when TreeNode::icon is set; nodes with no
277 // icon render exactly as before (label flush at label_x). Uses
278 // baseline placement (not GlyphCenter): compact geometric glyphs
279 // (◆ ▤ ⚙) sit too low under glyph-bbox centring, whereas a baseline
280 // at ~0.68 row-height aligns them with the label — matching the
281 // proven ListBox icon path.
282 if (!node.icon.empty()) {
283 canvas.set_font("system", 13);
284 canvas.set_fill_color(resolve_color("text_muted",
285 canvas::Color::hex(0x808090)));
287 canvas.fill_text(node.icon, label_x, y + row_height_ * 0.68f);
288 label_x += kIconWidth;
289 }
290
291 canvas.set_font("system", 13);
292 canvas.set_fill_color(
293 (is_sel && accent)
294 ? resolve_color("nav.selected.text", canvas::Color::hex(0x14b8a6))
295 : resolve_color("text", canvas::Color::hex(0xe0e0e0)));
297 canvas.fill_text_anchored(node.label, label_x, row_mid,
299
300 y += row_height_;
301 }
302
303 if (depth < 0 || node.expanded) {
304 for (auto& child : node.children) {
305 paint_node(canvas, *child, depth + 1, x, y, width);
306 }
307 }
308 }
309
310 int count_visible(const TreeNode& node, int depth) const {
311 int n = (depth >= 0) ? 1 : 0; // don't count the invisible root
312 if (depth < 0 || node.expanded) {
313 for (const auto& child : node.children) {
314 n += count_visible(*child, depth + 1);
315 }
316 }
317 return n;
318 }
319
320 TreeNode* find_by_user_data(TreeNode& node, void* data) {
321 if (node.user_data == data && data != nullptr) return &node;
322 for (auto& child : node.children) {
323 if (auto* found = find_by_user_data(*child, data)) return found;
324 }
325 return nullptr;
326 }
327
328 TreeNode* find_node_at_y(TreeNode& node, int depth, float& current_y, float target_y,
329 int* out_depth = nullptr) {
330 if (depth >= 0) {
331 if (target_y >= current_y && target_y < current_y + row_height_) {
332 if (out_depth) *out_depth = depth;
333 return &node;
334 }
335 current_y += row_height_;
336 }
337
338 if (depth < 0 || node.expanded) {
339 for (auto& child : node.children) {
340 auto* result = find_node_at_y(*child, depth + 1, current_y, target_y, out_depth);
341 if (result) return result;
342 }
343 }
344 return nullptr;
345 }
346};
347
348} // namespace pulp::view
Definition canvas.hpp:240
@ GlyphCenter
Definition canvas.hpp:1122
virtual void set_text_align(TextAlign align)=0
virtual void set_font(const std::string &family, float size)=0
virtual void set_fill_color(Color c)=0
virtual void fill_text_anchored(const std::string &text, float x, float y, TextAnchor anchor)
Definition canvas.hpp:1125
virtual void fill_text(const std::string &text, float x, float y)=0
void on_mouse_event(const MouseEvent &event) override
Mouse down with full event context.
Definition tree_view.hpp:148
bool wants_mouse_input() const override
True if this widget handles mouse down/drag through virtual methods even when it does not expose JS p...
Definition tree_view.hpp:198
void set_selection_style(SelectionStyle s)
Select the selection treatment (see SelectionStyle).
Definition tree_view.hpp:75
float scroll_offset() const
Current vertical scroll offset in pixels (0 = first row flush at top).
Definition tree_view.hpp:121
float max_scroll_offset() const
Largest valid scroll offset: 0 when the content fits, else the overflow past the viewport.
Definition tree_view.hpp:125
float indent() const
Indent per level in pixels.
Definition tree_view.hpp:92
std::function< void(TreeNode &node)> on_select
Called when a node is selected (clicked).
Definition tree_view.hpp:79
std::function< void(TreeNode &node)> on_activate
Called when a node is double-clicked (activated).
Definition tree_view.hpp:82
const TreeNode & root() const
Definition tree_view.hpp:71
void set_scroll_offset(float v)
Scroll to an absolute offset, clamped to [0, max_scroll_offset()] so the view can neither overscroll ...
Definition tree_view.hpp:132
TreeView()
Definition tree_view.hpp:67
TreeNode & root()
The invisible root node (its children are the top-level items).
Definition tree_view.hpp:70
bool on_key_event(const KeyEvent &event) override
Key event with modifiers and up/down state.
Definition tree_view.hpp:180
SelectionStyle
Selection treatment.
Definition tree_view.hpp:65
@ accent
Definition tree_view.hpp:65
@ standard
Definition tree_view.hpp:65
int visible_node_count() const
Number of rows currently painted (every node under an expanded ancestor; collapsed subtrees are exclu...
Definition tree_view.hpp:110
float content_height() const
Intrinsic content height in pixels: visible rows * row height.
Definition tree_view.hpp:116
void set_indent(float px)
Definition tree_view.hpp:93
std::function< void(TreeNode &node, bool expanded)> on_toggle
Called when a node is expanded or collapsed.
Definition tree_view.hpp:85
void set_row_height(float h)
Definition tree_view.hpp:89
void set_selected_node(TreeNode *node)
Programmatically select a node (without firing on_select).
Definition tree_view.hpp:99
TreeNode * find_node_by_user_data(void *data)
Find a node whose user_data matches the given pointer.
Definition tree_view.hpp:103
void paint(canvas::Canvas &canvas) override
Definition tree_view.hpp:136
TreeNode * selected_node() const
Get the currently selected node (nullptr if none).
Definition tree_view.hpp:96
SelectionStyle selection_style() const
Definition tree_view.hpp:76
float row_height() const
Row height in pixels.
Definition tree_view.hpp:88
Color resolve_color(const std::string &name, Color fallback={}) const
void request_repaint()
Request that this view's host invalidate and schedule a repaint.
void set_focusable(bool f)
Definition view.hpp:669
Rect local_bounds() const
Definition view.hpp:88
Input event types with modifier keys and pointer IDs.
Definition affine_transform.hpp:24
@ left
Definition canvas.hpp:221
@ glyph
GlyphAtlas — SDF / custom-text glyph cache.
Definition atlas_inventory.hpp:40
Hover-cursor tracking for platform window hosts.
Definition fwd.hpp:27
@ px
Absolute pixels.
Definition geometry.hpp:393
@ s
Definition input_events.hpp:41
@ h
Definition input_events.hpp:39
@ y
Definition input_events.hpp:42
@ n
Definition input_events.hpp:40
@ right
Definition input_events.hpp:53
@ left
Definition input_events.hpp:53
@ b
Definition input_events.hpp:39
@ x
Definition input_events.hpp:41
@ v
Definition input_events.hpp:41
@ height
Definition css_animation.hpp:128
@ width
Definition css_animation.hpp:127
static Color hex(uint32_t rgb)
Construct from hex 0xRRGGBB (alpha = 1.0).
Definition canvas.hpp:66
static constexpr Color rgba8(uint8_t r, uint8_t g, uint8_t b, uint8_t a=255)
Construct from 8-bit channels [0,255] — convenience for legacy/hex code.
Definition canvas.hpp:61
Rich key event with key code, modifiers, and repeat state.
Definition input_events.hpp:193
KeyCode key
Definition input_events.hpp:194
bool is_down
True for key-down, false for key-up.
Definition input_events.hpp:196
Rich mouse/pointer event with position, modifiers, button state, pointer ID, and stylus properties.
Definition input_events.hpp:104
bool is_wheel
True for scroll wheel events.
Definition input_events.hpp:114
bool is_down
True for mouse-down / touch-began events.
Definition input_events.hpp:111
float scroll_delta_y
Vertical scroll delta (positive = down).
Definition input_events.hpp:116
int click_count
1=single, 2=double, 3=triple click
Definition input_events.hpp:110
Point position
Position in local view coordinates.
Definition input_events.hpp:105
float x
Definition geometry.hpp:21
A node in the tree hierarchy.
Definition tree_view.hpp:18
void toggle()
Toggle expanded state.
Definition tree_view.hpp:38
void * user_data
Arbitrary payload.
Definition tree_view.hpp:23
bool expanded
Whether children are visible.
Definition tree_view.hpp:21
std::string icon
Optional icon name.
Definition tree_view.hpp:20
TreeNode & add_child(std::string child_label)
Add a child node and return a reference to it.
Definition tree_view.hpp:28
std::string label
Definition tree_view.hpp:19
std::vector< std::unique_ptr< TreeNode > > children
Definition tree_view.hpp:25
bool selectable
Definition tree_view.hpp:22
bool has_children() const
Check if this node has children.
Definition tree_view.hpp:35