imgui_debugger 0.1.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,170 @@
1
+ """The tree's filter: a bounded, memoized match over a value and its children.
2
+
3
+ The walk is capped in width and depth so a multi-megabyte attribute dict cannot
4
+ cost milliseconds per frame, and results are cached for the lifetime of one
5
+ filter string.
6
+
7
+ Examples
8
+ --------
9
+ >>> from imgui_debugger.search import matches
10
+ >>> matches("metadata", {"fs": 9.6}, "fs")
11
+ True
12
+ >>> matches("metadata", {"fs": 9.6}, "dz")
13
+ False
14
+ """
15
+
16
+ from __future__ import annotations
17
+
18
+ from collections.abc import Mapping, Sequence
19
+
20
+ MAX_ITEMS = 64
21
+ MAX_DEPTH = 6
22
+ CACHE_MAX = 4096
23
+
24
+ _cache: dict = {}
25
+ _cache_filter: str | None = None
26
+
27
+
28
+ def clear_cache() -> None:
29
+ """Drop every memoized match result.
30
+
31
+ Examples
32
+ --------
33
+ >>> from imgui_debugger.search import clear_cache, matches
34
+ >>> matches("a", 1, "a")
35
+ True
36
+ >>> clear_cache()
37
+ """
38
+ global _cache_filter
39
+ _cache.clear()
40
+ _cache_filter = None
41
+
42
+
43
+ def matches_shallow(name, value, text: str) -> bool:
44
+ """Whether a name, or a non-container value, contains ``text``.
45
+
46
+ Parameters
47
+ ----------
48
+ name : object
49
+ The row label.
50
+ value : object
51
+ The row value.
52
+ text : str
53
+ Filter text; an empty string matches everything.
54
+
55
+ Examples
56
+ --------
57
+ >>> from imgui_debugger.search import matches_shallow
58
+ >>> matches_shallow("frame_rate", 30, "rate")
59
+ True
60
+ >>> matches_shallow("frame_rate", 30, "30")
61
+ True
62
+ >>> matches_shallow("frame_rate", [30], "30")
63
+ False
64
+ """
65
+ if not text:
66
+ return True
67
+ low = text.lower()
68
+ if low in str(name).lower():
69
+ return True
70
+ if isinstance(value, (Mapping, list, tuple)):
71
+ return False
72
+ try:
73
+ return low in str(value).lower()
74
+ except Exception:
75
+ return False
76
+
77
+
78
+ def matches(name, value, text: str, depth: int = 0) -> bool:
79
+ """Whether a row or any of its bounded descendants matches ``text``.
80
+
81
+ Parameters
82
+ ----------
83
+ name : object
84
+ The row label.
85
+ value : object
86
+ The row value.
87
+ text : str
88
+ Filter text; an empty string matches everything.
89
+ depth : int
90
+ Current recursion depth, used internally.
91
+
92
+ Examples
93
+ --------
94
+ >>> from imgui_debugger.search import matches
95
+ >>> matches("arr", {"meta": {"dz": 5}}, "dz")
96
+ True
97
+ >>> matches("arr", [1, 2, 3], "2")
98
+ True
99
+ >>> matches("arr", "hello", "ell")
100
+ True
101
+ """
102
+ global _cache_filter
103
+ if not text:
104
+ return True
105
+ if text != _cache_filter:
106
+ _cache.clear()
107
+ _cache_filter = text
108
+ key = (str(name), id(value), depth)
109
+ hit = _cache.get(key)
110
+ if hit is not None and hit[0] is value:
111
+ return hit[1]
112
+ result = _walk(name, value, text, depth)
113
+ if len(_cache) >= CACHE_MAX:
114
+ _cache.clear()
115
+ _cache[key] = (value, result)
116
+ return result
117
+
118
+
119
+ def _walk(name, value, text: str, depth: int) -> bool:
120
+ """Do the bounded recursive match behind :func:`matches`.
121
+
122
+ Examples
123
+ --------
124
+ >>> from imgui_debugger.search import _walk
125
+ >>> _walk("cfg", {"a": {"b": 1}}, "b", 0)
126
+ True
127
+ >>> _walk("cfg", {"a": {"b": 1}}, "b", 6)
128
+ False
129
+ """
130
+ if matches_shallow(name, value, text):
131
+ return True
132
+ if depth >= MAX_DEPTH:
133
+ return False
134
+ if isinstance(value, Mapping):
135
+ for i, (k, v) in enumerate(value.items()):
136
+ if i >= MAX_ITEMS:
137
+ break
138
+ if matches(k, v, text, depth + 1):
139
+ return True
140
+ return False
141
+ if isinstance(value, Sequence) and not isinstance(value, (str, bytes, bytearray)):
142
+ for i, v in enumerate(value):
143
+ if i >= MAX_ITEMS:
144
+ break
145
+ if matches(f"[{i}]", v, text, depth + 1):
146
+ return True
147
+ return False
148
+
149
+
150
+ def filter_children(children, text: str):
151
+ """Keep only the rows that match ``text``.
152
+
153
+ Parameters
154
+ ----------
155
+ children : list[imgui_debugger.scopes.Child]
156
+ Rows to filter.
157
+ text : str
158
+ Filter text; an empty string keeps everything.
159
+
160
+ Examples
161
+ --------
162
+ >>> from imgui_debugger.scopes import Child
163
+ >>> from imgui_debugger.search import filter_children
164
+ >>> rows = [Child("fs", 9.6), Child("dz", 5)]
165
+ >>> [c.name for c in filter_children(rows, "dz")]
166
+ ['dz']
167
+ """
168
+ if not text:
169
+ return list(children)
170
+ return [c for c in children if matches(c.name, c.value, text)]
@@ -0,0 +1,129 @@
1
+ """Colors and rounding knobs for the debugger tree.
2
+
3
+ Examples
4
+ --------
5
+ >>> from imgui_debugger import Theme
6
+ >>> Theme.dark().replace(value=(1.0, 1.0, 1.0, 1.0)).value
7
+ (1.0, 1.0, 1.0, 1.0)
8
+ """
9
+
10
+ from __future__ import annotations
11
+
12
+ from dataclasses import dataclass, replace
13
+ from typing import Tuple
14
+
15
+ from imgui_bundle import imgui
16
+
17
+ Color = Tuple[float, float, float, float]
18
+
19
+
20
+ def to_vec4(color) -> "imgui.ImVec4":
21
+ """Coerce an ``(r, g, b[, a])`` tuple (or an ImVec4) to ``imgui.ImVec4``.
22
+
23
+ Parameters
24
+ ----------
25
+ color : tuple | imgui.ImVec4
26
+ Components in the 0..1 range.
27
+
28
+ Examples
29
+ --------
30
+ >>> from imgui_debugger import to_vec4
31
+ >>> to_vec4((1.0, 0.0, 0.0)).w
32
+ 1.0
33
+ """
34
+ if isinstance(color, imgui.ImVec4):
35
+ return color
36
+ r, g, b = color[0], color[1], color[2]
37
+ a = color[3] if len(color) > 3 else 1.0
38
+ return imgui.ImVec4(r, g, b, a)
39
+
40
+
41
+ @dataclass(frozen=True)
42
+ class Theme:
43
+ """Palette for the debugger, one ``(r, g, b, a)`` float tuple per role.
44
+
45
+ Examples
46
+ --------
47
+ >>> from imgui_debugger import Theme
48
+ >>> t = Theme.light()
49
+ >>> t.node != Theme.dark().node
50
+ True
51
+ >>> Theme.dark().replace(accent=(1.0, 0.5, 0.0, 1.0)).accent
52
+ (1.0, 0.5, 0.0, 1.0)
53
+ """
54
+
55
+ bg: Color = (0.11, 0.11, 0.12, 1.0)
56
+ text: Color = (0.90, 0.90, 0.92, 1.0)
57
+ text_dim: Color = (0.55, 0.55, 0.58, 1.0)
58
+ accent: Color = (0.20, 0.50, 0.85, 1.0)
59
+ border: Color = (0.35, 0.35, 0.37, 0.7)
60
+ separator: Color = (0.35, 0.35, 0.37, 0.6)
61
+ frame_bg: Color = (0.18, 0.18, 0.20, 1.0)
62
+ # tree roles
63
+ node: Color = (0.40, 0.80, 0.95, 1.0)
64
+ name: Color = (0.95, 0.80, 0.30, 1.0)
65
+ index: Color = (0.60, 0.95, 0.40, 1.0)
66
+ value: Color = (0.85, 0.85, 0.85, 1.0)
67
+ prop: Color = (0.70, 0.75, 1.00, 1.0)
68
+ cls: Color = (0.85, 0.65, 1.00, 1.0)
69
+ local: Color = (0.95, 0.80, 0.30, 1.0)
70
+ glob: Color = (0.55, 0.85, 0.80, 1.0)
71
+ runtime: Color = (0.90, 0.60, 0.40, 1.0)
72
+ error: Color = (0.95, 0.40, 0.40, 1.0)
73
+ changed: Color = (1.00, 0.85, 0.35, 1.0)
74
+ frame_rounding: float = 4.0
75
+ child_rounding: float = 4.0
76
+
77
+ @staticmethod
78
+ def dark() -> "Theme":
79
+ """The default dark palette.
80
+
81
+ Examples
82
+ --------
83
+ >>> from imgui_debugger import Theme
84
+ >>> Theme.dark() == Theme()
85
+ True
86
+ """
87
+ return Theme()
88
+
89
+ @staticmethod
90
+ def light() -> "Theme":
91
+ """A light palette with the same role names.
92
+
93
+ Examples
94
+ --------
95
+ >>> from imgui_debugger import Theme
96
+ >>> Theme.light().bg[0] > 0.5
97
+ True
98
+ """
99
+ return Theme(
100
+ bg=(0.94, 0.94, 0.95, 1.0),
101
+ text=(0.10, 0.10, 0.12, 1.0),
102
+ text_dim=(0.42, 0.42, 0.46, 1.0),
103
+ accent=(0.10, 0.45, 0.80, 1.0),
104
+ border=(0.70, 0.70, 0.74, 0.9),
105
+ separator=(0.70, 0.70, 0.74, 0.6),
106
+ frame_bg=(0.88, 0.88, 0.90, 1.0),
107
+ node=(0.05, 0.40, 0.60, 1.0),
108
+ name=(0.55, 0.38, 0.05, 1.0),
109
+ index=(0.15, 0.45, 0.10, 1.0),
110
+ value=(0.18, 0.18, 0.20, 1.0),
111
+ prop=(0.25, 0.30, 0.65, 1.0),
112
+ cls=(0.45, 0.20, 0.60, 1.0),
113
+ local=(0.55, 0.38, 0.05, 1.0),
114
+ glob=(0.10, 0.45, 0.42, 1.0),
115
+ runtime=(0.65, 0.35, 0.10, 1.0),
116
+ error=(0.75, 0.15, 0.15, 1.0),
117
+ changed=(0.60, 0.42, 0.00, 1.0),
118
+ )
119
+
120
+ def replace(self, **changes) -> "Theme":
121
+ """Return a copy with the given fields overridden.
122
+
123
+ Examples
124
+ --------
125
+ >>> from imgui_debugger import Theme
126
+ >>> Theme.dark().replace(name=(1.0, 0.0, 0.0, 1.0)).name
127
+ (1.0, 0.0, 0.0, 1.0)
128
+ """
129
+ return replace(self, **changes)
imgui_debugger/tree.py ADDED
@@ -0,0 +1,285 @@
1
+ """The collapsible tree: one scope header per root, one row per value below it.
2
+
3
+ Examples
4
+ --------
5
+ >>> from imgui_debugger.tree import TreeStyle
6
+ >>> TreeStyle(filter="fs").filter
7
+ 'fs'
8
+ """
9
+
10
+ from __future__ import annotations
11
+
12
+ from dataclasses import dataclass, field
13
+ from typing import Optional
14
+
15
+ from imgui_bundle import imgui
16
+
17
+ from .edit import can_edit, edit_value
18
+ from .format import fmt_error, fmt_value, type_label
19
+ from .scopes import Child, Scope, children_of, has_children
20
+ from .search import filter_children, matches
21
+ from .theme import Theme, to_vec4
22
+
23
+ ROLE_COLORS = {
24
+ "attr": "name",
25
+ "prop": "prop",
26
+ "class": "cls",
27
+ "item": "name",
28
+ "index": "index",
29
+ "local": "local",
30
+ "global": "glob",
31
+ "runtime": "runtime",
32
+ "error": "error",
33
+ }
34
+
35
+
36
+ @dataclass(frozen=True)
37
+ class TreeStyle:
38
+ """Everything the row renderer needs besides the rows themselves.
39
+
40
+ Parameters
41
+ ----------
42
+ theme : Theme
43
+ Palette.
44
+ filter : str
45
+ Case-insensitive filter; empty shows everything.
46
+ editable : bool
47
+ Draw inline editors for writable leaves.
48
+ private : bool
49
+ Expand ``_name`` attributes of nested values.
50
+ properties : bool
51
+ Expand ``property`` descriptors of nested values.
52
+ max_depth : int
53
+ Deepest level expanded before a row is shown as a summary only.
54
+ max_items : int
55
+ Rows rendered per container before a "+N more" line.
56
+ value_col : float
57
+ Pixel column the value is drawn at; ``0`` packs it after the name.
58
+ force_open : bool | None
59
+ For one frame, expand (True) or collapse (False) every node.
60
+
61
+ Examples
62
+ --------
63
+ >>> from imgui_debugger.tree import TreeStyle
64
+ >>> from imgui_debugger import Theme
65
+ >>> TreeStyle(theme=Theme.light(), value_col=220.0).value_col
66
+ 220.0
67
+ """
68
+
69
+ theme: Theme = field(default_factory=Theme.dark)
70
+ filter: str = ""
71
+ editable: bool = True
72
+ private: bool = False
73
+ properties: bool = True
74
+ max_depth: int = 8
75
+ max_items: int = 200
76
+ value_col: float = 0.0
77
+ force_open: Optional[bool] = None
78
+
79
+
80
+ def role_color(style: TreeStyle, kind: str):
81
+ """The theme color a row of the given kind is drawn in.
82
+
83
+ Parameters
84
+ ----------
85
+ style : TreeStyle
86
+ Active style.
87
+ kind : str
88
+ A :class:`~imgui_debugger.scopes.Child` kind, or a theme field name.
89
+
90
+ Examples
91
+ --------
92
+ >>> from imgui_debugger.tree import TreeStyle, role_color
93
+ >>> role_color(TreeStyle(), "index") == TreeStyle().theme.index
94
+ True
95
+ """
96
+ name = ROLE_COLORS.get(kind, kind)
97
+ return getattr(style.theme, name, style.theme.name)
98
+
99
+
100
+ def draw_value(child: Child, path: str, style: TreeStyle) -> None:
101
+ """Draw a leaf's value, as an editor when it is writable and editable.
102
+
103
+ Parameters
104
+ ----------
105
+ child : Child
106
+ The row.
107
+ path : str
108
+ Dotted path, used as the imgui id.
109
+ style : TreeStyle
110
+ Active style.
111
+
112
+ Examples
113
+ --------
114
+ >>> from imgui_debugger.scopes import Child
115
+ >>> from imgui_debugger.tree import TreeStyle, draw_value
116
+ >>> draw_value(Child("fs", 9.6), "arr.fs", TreeStyle()) # doctest: +SKIP
117
+ """
118
+ if style.value_col > 0:
119
+ imgui.same_line(style.value_col)
120
+ else:
121
+ imgui.same_line(spacing=16)
122
+ if child.kind == "error":
123
+ imgui.text_colored(to_vec4(style.theme.error), fmt_error(child.value))
124
+ return
125
+ if style.editable and child.editable and can_edit(child.value):
126
+ edit_value(path, child.value, child.setter, style.theme)
127
+ return
128
+ imgui.text_colored(to_vec4(style.theme.value), fmt_value(child.value))
129
+ if imgui.is_item_hovered():
130
+ imgui.set_tooltip(f"{path}\n{type_label(child.value)}")
131
+
132
+
133
+ def draw_child(child: Child, prefix: str, depth: int, style: TreeStyle) -> None:
134
+ """Draw one row, recursing into its children when it expands.
135
+
136
+ Parameters
137
+ ----------
138
+ child : Child
139
+ The row.
140
+ prefix : str
141
+ Dotted path of the parent, ``""`` at a scope root.
142
+ depth : int
143
+ Current depth, compared against ``style.max_depth``.
144
+ style : TreeStyle
145
+ Active style.
146
+
147
+ Examples
148
+ --------
149
+ >>> from imgui_debugger.scopes import Child
150
+ >>> from imgui_debugger.tree import TreeStyle, draw_child
151
+ >>> draw_child(Child("md", {"fs": 9.6}), "", 0, TreeStyle()) # doctest: +SKIP
152
+ """
153
+ path = f"{prefix}{child.name}" if child.name.startswith("[") else f"{prefix}.{child.name}"
154
+ path = path.lstrip(".")
155
+ color = role_color(style, child.kind)
156
+ expandable = (
157
+ child.kind != "error"
158
+ and depth < style.max_depth
159
+ and has_children(child.value, style.private, style.properties)
160
+ )
161
+
162
+ if not expandable:
163
+ imgui.text_colored(to_vec4(color), child.name)
164
+ draw_value(child, path, style)
165
+ return
166
+
167
+ if style.force_open is not None:
168
+ imgui.set_next_item_open(style.force_open)
169
+ imgui.push_style_color(imgui.Col_.text, to_vec4(style.theme.node))
170
+ open_ = imgui.tree_node(f"{child.name}##{path}")
171
+ imgui.pop_style_color()
172
+ if imgui.is_item_hovered():
173
+ imgui.set_tooltip(f"{path}\n{type_label(child.value)}")
174
+ imgui.same_line(style.value_col if style.value_col > 0 else 0.0)
175
+ imgui.text_colored(to_vec4(style.theme.text_dim), type_label(child.value))
176
+ if not open_:
177
+ return
178
+ try:
179
+ rows = children_of(child.value, style.private, style.properties, style.max_items + 1)
180
+ except Exception as exc:
181
+ imgui.text_colored(to_vec4(style.theme.error), fmt_error(exc))
182
+ imgui.tree_pop()
183
+ return
184
+ draw_children(rows, path, depth + 1, style)
185
+ imgui.tree_pop()
186
+
187
+
188
+ def draw_children(rows, prefix: str, depth: int, style: TreeStyle) -> None:
189
+ """Draw a list of rows, filtered and capped at ``style.max_items``.
190
+
191
+ Parameters
192
+ ----------
193
+ rows : list[Child]
194
+ Rows to draw.
195
+ prefix : str
196
+ Dotted path of the parent.
197
+ depth : int
198
+ Current depth.
199
+ style : TreeStyle
200
+ Active style.
201
+
202
+ Examples
203
+ --------
204
+ >>> from imgui_debugger.scopes import Child
205
+ >>> from imgui_debugger.tree import TreeStyle, draw_children
206
+ >>> draw_children([Child("n", 1)], "cfg", 1, TreeStyle()) # doctest: +SKIP
207
+ """
208
+ shown = filter_children(rows, style.filter)
209
+ for child in shown[: style.max_items]:
210
+ draw_child(child, prefix, depth, style)
211
+ hidden = len(shown) - style.max_items
212
+ if hidden > 0:
213
+ imgui.text_disabled(f"... +{hidden} more")
214
+
215
+
216
+ def draw_scope(scope: Scope, style: TreeStyle) -> str:
217
+ """Draw one scope header and its rows, returning the scope name.
218
+
219
+ Parameters
220
+ ----------
221
+ scope : Scope
222
+ The scope to draw.
223
+ style : TreeStyle
224
+ Active style.
225
+
226
+ Returns
227
+ -------
228
+ str
229
+ ``scope.name``, so a caller can key per-scope state off the return.
230
+
231
+ Examples
232
+ --------
233
+ >>> from imgui_debugger.scopes import Scope, Child
234
+ >>> from imgui_debugger.tree import TreeStyle, draw_scope
235
+ >>> s = Scope("counters", lambda: [Child("frames", 1)])
236
+ >>> draw_scope(s, TreeStyle()) # doctest: +SKIP
237
+ 'counters'
238
+ """
239
+ try:
240
+ rows = scope.children()
241
+ except Exception as exc:
242
+ imgui.text_colored(to_vec4(style.theme.error), f"{scope.name}: {fmt_error(exc)}")
243
+ return scope.name
244
+ shown = filter_children(rows, style.filter)
245
+ if style.filter and not shown:
246
+ return scope.name
247
+
248
+ if style.force_open is not None:
249
+ imgui.set_next_item_open(style.force_open)
250
+ elif style.filter:
251
+ imgui.set_next_item_open(True)
252
+ else:
253
+ imgui.set_next_item_open(scope.start_open, imgui.Cond_.first_use_ever)
254
+ imgui.push_style_color(imgui.Col_.text, to_vec4(role_color(style, scope.role)))
255
+ open_ = imgui.collapsing_header(f"{scope.name}##scope_{scope.name}")
256
+ imgui.pop_style_color()
257
+ if scope.hint and imgui.is_item_hovered():
258
+ imgui.set_tooltip(scope.hint)
259
+ imgui.same_line()
260
+ imgui.text_disabled(f"({len(shown)})")
261
+ if open_:
262
+ imgui.indent()
263
+ draw_children(shown, scope.name, 1, style)
264
+ imgui.unindent()
265
+ return scope.name
266
+
267
+
268
+ def row_matches(child: Child, text: str) -> bool:
269
+ """Whether one row survives the filter, children included.
270
+
271
+ Parameters
272
+ ----------
273
+ child : Child
274
+ The row.
275
+ text : str
276
+ Filter text.
277
+
278
+ Examples
279
+ --------
280
+ >>> from imgui_debugger.scopes import Child
281
+ >>> from imgui_debugger.tree import row_matches
282
+ >>> row_matches(Child("md", {"fs": 9.6}), "fs")
283
+ True
284
+ """
285
+ return matches(child.name, child.value, text)