diff --git a/scripts/gdb/lvglgdb/__init__.py b/scripts/gdb/lvglgdb/__init__.py index d78849731f..8eb6094483 100644 --- a/scripts/gdb/lvglgdb/__init__.py +++ b/scripts/gdb/lvglgdb/__init__.py @@ -44,6 +44,8 @@ from .lvgl import ( INDEV_TYPE_NAMES, LVGroup, LVObjClass, + OBJ_FLAG_NAMES, + decode_obj_flags, LVSubject, LVObserver, SUBJECT_TYPE_NAMES, @@ -95,6 +97,8 @@ __all__ = [ "INDEV_TYPE_NAMES", "LVGroup", "LVObjClass", + "OBJ_FLAG_NAMES", + "decode_obj_flags", "LVSubject", "LVObserver", "SUBJECT_TYPE_NAMES", diff --git a/scripts/gdb/lvglgdb/cmds/dashboard/static/uinspy.html b/scripts/gdb/lvglgdb/cmds/dashboard/static/uinspy.html index e34adaab78..83bf944289 100644 --- a/scripts/gdb/lvglgdb/cmds/dashboard/static/uinspy.html +++ b/scripts/gdb/lvglgdb/cmds/dashboard/static/uinspy.html @@ -1219,6 +1219,24 @@ ui-topbar .topbar-search::placeholder { .detail-style-table td { padding: 1px 4px; } +.detail-flags-wrap { + display: flex; + flex-wrap: wrap; + gap: calc(var(--spacing) * 1); +} +.detail-flag-badge { + display: inline-block; + border-radius: 0.25rem; + background-color: var(--color-surface0); + padding-inline: calc(var(--spacing) * 1.5); + padding-block: 1px; + font-family: var(--font-mono); + font-size: 10px; + --tw-font-weight: var(--font-weight-medium); + font-weight: var(--font-weight-medium); + color: var(--color-overlay1); + border: 1px solid var(--surface0); +} .scene-controls { margin-bottom: calc(var(--spacing) * 1); display: flex; @@ -2057,7 +2075,9 @@ function renderObjTree(obj, depth = 0) { if (det.className = "obj-node", obj.addr) det.id = "obj-" + obj.addr; let sum = document.createElement("summary"); - if (sum.style.setProperty("--depth-color", DEPTH_COLORS[depth % DEPTH_COLORS.length]), sum.textContent = obj.class_name || "obj", det.appendChild(sum), obj.addr) + if (sum.style.setProperty("--depth-color", DEPTH_COLORS[depth % DEPTH_COLORS.length]), sum.textContent = obj.class_name || "obj", obj.flags_list?.includes("HIDDEN")) + sum.textContent += " \uD83D\uDC41‍\uD83D\uDDE8"; + if (det.appendChild(sum), obj.addr) objDataMap[obj.addr] = obj, registerHL(obj.addr, det), sum.addEventListener("mouseenter", () => highlightObj(obj.addr)), sum.addEventListener("mouseleave", () => clearHighlight()), sum.addEventListener("click", (e) => { e.stopPropagation(), selectObj(obj.addr); }), sum.addEventListener("dblclick", (e) => { @@ -2105,7 +2125,12 @@ function renderObjDetail(addr, panel) { let _e10 = document.createElement("span"); return _e10.setAttribute("class", "detail-coord-val"), _e10.append(String(w), " × ", String(h)), _e8.append(_e9, _e10), _e2.append(_e3, _e8), _e.append(_e1, _e2), _e; })(); - panel.appendChild(coordSec); + if (panel.appendChild(coordSec), obj.flags_list?.length) { + let flagSec = el("div", "detail-section"); + flagSec.appendChild(el("div", "detail-section-title", "Flags")); + let wrap = el("div", "detail-flags-wrap"); + obj.flags_list.forEach((f) => wrap.appendChild(el("span", "detail-flag-badge", f))), flagSec.appendChild(wrap), panel.appendChild(flagSec); + } let refSec = el("div", "detail-section"); if (refSec.appendChild(el("div", "detail-section-title", "References")), obj.parent_addr) { let row = el("div", "kv-row"); @@ -2280,8 +2305,8 @@ function flattenLayers(trees) { let idx = sIdx++; screenNames.push(s.layer_name || s.class_name || "screen_" + idx); let maxLocal = 0; - function walk(obj, ld) { - let c = obj.coords || { x1: 0, y1: 0, x2: 0, y2: 0 }; + function walk(obj, ld, parentHidden) { + let c = obj.coords || { x1: 0, y1: 0, x2: 0, y2: 0 }, hidden = parentHidden || (obj.flags_list?.includes("HIDDEN") ?? !1); layers.push({ addr: obj.addr, class_name: obj.class_name || "obj", @@ -2293,10 +2318,11 @@ function flattenLayers(trees) { localDepth: ld, child_count: obj.child_count || 0, style_count: obj.style_count || 0, - screenIdx: idx - }), maxLocal = Math.max(maxLocal, ld), obj.children?.forEach((ch) => walk(ch, ld + 1)); + screenIdx: idx, + hidden + }), maxLocal = Math.max(maxLocal, ld), obj.children?.forEach((ch) => walk(ch, ld + 1, hidden)); } - walk(s, 0), screenMaxLocal[idx] = maxLocal, globalOffset += maxLocal + C.SCREEN_GAP; + walk(s, 0, !1), screenMaxLocal[idx] = maxLocal, globalOffset += maxLocal + C.SCREEN_GAP; })), { layers, screenNames, screenMaxLocal }; } function resolveCSSColor(cssVar) { @@ -2340,7 +2366,9 @@ function build3DScene(container, trees, displays, dispObjs) { btn.dataset.on = v ? "0" : "1", btn.classList.toggle("active", !v); }, btn; }, controls = el("div", "scene-controls"), toggle3d = makeToggle("3D", !0), toggleBorders = makeToggle("Borders", !0), bufToggles = [], toggleOrtho = makeToggle("Ortho", !1); - controls.append(toggle3d, toggleBorders), bufImages.forEach((bi) => { + controls.append(toggle3d, toggleBorders); + let toggleHidden = makeToggle("Hidden", !1); + controls.appendChild(toggleHidden), bufImages.forEach((bi) => { let btn = makeToggle(bi.label, !0), thumb = (() => { let _e = document.createElement("img"); return _e.setAttribute("draggable", "false"), _e.setAttribute("style", "height:1.2em;border-radius:2px;vertical-align:middle;image-rendering:pixelated"), _e; @@ -2474,7 +2502,7 @@ function build3DScene(container, trees, displays, dispObjs) { depthRange.min = visMax, depthMinSlider.value = String(visMax); } function updateDepths(spreadOv, rangeOv) { - let bordersOn = toggleBorders.dataset.on === "1", range = rangeOv ?? depthRange, screenOffset = computeScreenOffsets(); + let bordersOn = toggleBorders.dataset.on === "1", showHidden = toggleHidden.dataset.on === "1", range = rangeOv ?? depthRange, screenOffset = computeScreenOffsets(); currentSpread = spreadOv ?? (is3d ? Number(spreadSlider.value) : 0.1); let compressedIdx = 0, compressedDepth = {}, seenDepths = /* @__PURE__ */ new Set; layers.forEach((l) => { @@ -2483,7 +2511,7 @@ function build3DScene(container, trees, displays, dispObjs) { seenDepths.add(gd), compressedDepth[gd] = compressedIdx++; }), layers.forEach((l, idx) => { let sl = sceneLayers[idx], gd = screenOffset[l.screenIdx] !== void 0 ? screenOffset[l.screenIdx] + l.localDepth : -1, inRange = gd >= Math.round(range.min) && gd <= Math.round(range.max); - sl.visible = bordersOn && layerVisible[l.screenIdx] && inRange, sl.depth = (compressedDepth[gd] ?? 0) * currentSpread; + sl.visible = bordersOn && layerVisible[l.screenIdx] && inRange && (!l.hidden || showHidden), sl.depth = (compressedDepth[gd] ?? 0) * currentSpread; }), sceneBufs.forEach((b) => { b.depth = -currentSpread * 0.5; }), renderer.markDirty(); @@ -2551,7 +2579,7 @@ function build3DScene(container, trees, displays, dispObjs) { }), toggleOrtho.addEventListener("click", () => { let ortho = toggleOrtho.dataset.on === "1"; animateTo({ persp: ortho ? 0 : 1 }); - }), toggleBorders.addEventListener("click", () => updateVisibility()), bufToggles.forEach((btn) => btn.addEventListener("click", () => updateVisibility())); + }), toggleBorders.addEventListener("click", () => updateVisibility()), toggleHidden.addEventListener("click", () => updateVisibility()), bufToggles.forEach((btn) => btn.addEventListener("click", () => updateVisibility())); let dragging = !1, lastX = 0, lastY = 0; viewport.onmousedown = (e) => { if (inScreensaver) @@ -2721,7 +2749,7 @@ function build3DScene(container, trees, displays, dispObjs) { btn.dataset.on = on ? "1" : "0", btn.classList.toggle("active", on); }; return resetBtn.onclick = () => { - is3d = !0, setToggle(toggle3d, !0), setToggle(toggleBorders, !0), setToggle(toggleOrtho, !1), bufToggles.forEach((btn) => setToggle(btn, !0)), screenNames.forEach((name, i) => { + is3d = !0, setToggle(toggle3d, !0), setToggle(toggleBorders, !0), setToggle(toggleOrtho, !1), setToggle(toggleHidden, !1), bufToggles.forEach((btn) => setToggle(btn, !0)), screenNames.forEach((name, i) => { layerVisible[i] = name === "act_scr" || screenNames.length === 1; }), layerBtns.forEach((b, i) => b.classList.toggle("active", layerVisible[i])), spreadSlider.disabled = !1, focusedAddr = null, updateDepthSliderRange(), updateVisibility(); let visMax = Number(depthMaxSlider.max); @@ -3085,7 +3113,7 @@ class UiDashboard extends BaseComponent { customElements.define("ui-dashboard", UiDashboard); // src/app.ts -document.getElementById("about").innerHTML = `uinspy v${"0.4.6"}·Built ${"2026-03-27 20:04 GMT+8"}·${"14ea6ea"}·${"Canvas2D"}·GitHub·LVGL·MIT`; +document.getElementById("about").innerHTML = `uinspy v${"0.4.6"}·Built ${"2026-04-01 13:27 GMT+8"}·${"8f35af9"}·${"Canvas2D"}·GitHub·LVGL·MIT`; \ No newline at end of file diff --git a/scripts/gdb/lvglgdb/lvgl/__init__.py b/scripts/gdb/lvglgdb/lvgl/__init__.py index c89d71b3b6..6ebb3f3e2c 100644 --- a/scripts/gdb/lvglgdb/lvgl/__init__.py +++ b/scripts/gdb/lvglgdb/lvgl/__init__.py @@ -8,6 +8,8 @@ from .core import ( INDEV_TYPE_NAMES, LVGroup, LVObjClass, + OBJ_FLAG_NAMES, + decode_obj_flags, LVSubject, LVObserver, SUBJECT_TYPE_NAMES, @@ -96,6 +98,8 @@ __all__ = [ "INDEV_TYPE_NAMES", "LVGroup", "LVObjClass", + "OBJ_FLAG_NAMES", + "decode_obj_flags", "LVSubject", "LVObserver", "SUBJECT_TYPE_NAMES", diff --git a/scripts/gdb/lvglgdb/lvgl/core/__init__.py b/scripts/gdb/lvglgdb/lvgl/core/__init__.py index c4e1c0b4c2..b353bbfce2 100644 --- a/scripts/gdb/lvglgdb/lvgl/core/__init__.py +++ b/scripts/gdb/lvglgdb/lvgl/core/__init__.py @@ -3,6 +3,7 @@ from .lv_global import curr_inst from .lv_indev import LVIndev, INDEV_TYPE_NAMES from .lv_group import LVGroup from .lv_obj_class import LVObjClass +from .lv_obj_flag_consts import OBJ_FLAG_NAMES, decode_obj_flags from .lv_observer import LVSubject, LVObserver from .lv_observer_consts import SUBJECT_TYPE_NAMES @@ -16,6 +17,8 @@ __all__ = [ "INDEV_TYPE_NAMES", "LVGroup", "LVObjClass", + "OBJ_FLAG_NAMES", + "decode_obj_flags", "LVSubject", "LVObserver", "SUBJECT_TYPE_NAMES", diff --git a/scripts/gdb/lvglgdb/lvgl/core/lv_obj.py b/scripts/gdb/lvglgdb/lvgl/core/lv_obj.py index dcefc6e1db..359bef3c72 100644 --- a/scripts/gdb/lvglgdb/lvgl/core/lv_obj.py +++ b/scripts/gdb/lvglgdb/lvgl/core/lv_obj.py @@ -89,6 +89,18 @@ class LVObject(Value): def y2(self): return int(self.coords.y2) + @property + def flags_raw(self) -> int: + """Return raw flags bitmask, 0 if corrupted.""" + raw = self.safe_field("flags", 0) + return int(raw) + + @property + def flags_list(self) -> list[str]: + """Return decoded flag names.""" + from .lv_obj_flag_consts import decode_obj_flags + return decode_obj_flags(self.flags_raw) + @property def child_cnt(self) -> int: """Return child count, 0 if corrupted.""" @@ -153,6 +165,8 @@ class LVObject(Value): }, {"x1": 0, "y1": 0, "x2": 0, "y2": 0}), ("child_count", lambda s: s.child_cnt, 0), ("style_count", lambda s: int(s.style_cnt), 0), + ("flags", lambda s: s.flags_raw, 0), + ("flags_list", lambda s: s.flags_list, []), ("parent_addr", lambda s: ptr_or_none(s.super_value("parent"))), ("group_addr", lambda s: s._get_group_addr()), ]) diff --git a/scripts/gdb/lvglgdb/lvgl/core/lv_obj_flag_consts.py b/scripts/gdb/lvglgdb/lvgl/core/lv_obj_flag_consts.py new file mode 100644 index 0000000000..4278983eda --- /dev/null +++ b/scripts/gdb/lvglgdb/lvgl/core/lv_obj_flag_consts.py @@ -0,0 +1,44 @@ +""" +Auto-generated object flag constants from LVGL headers. + +Do not edit manually. Regenerate from the GDB script root with: + python3 scripts/generate_all.py +""" + +OBJ_FLAG_NAMES = { + 1: "HIDDEN", + 2: "CLICKABLE", + 4: "CLICK_FOCUSABLE", + 8: "CHECKABLE", + 16: "SCROLLABLE", + 32: "SCROLL_ELASTIC", + 64: "SCROLL_MOMENTUM", + 128: "SCROLL_ONE", + 256: "SCROLL_CHAIN_HOR", + 512: "SCROLL_CHAIN_VER", + 1024: "SCROLL_ON_FOCUS", + 2048: "SCROLL_WITH_ARROW", + 4096: "SNAPPABLE", + 8192: "PRESS_LOCK", + 16384: "EVENT_BUBBLE", + 32768: "GESTURE_BUBBLE", + 65536: "ADV_HITTEST", + 131072: "IGNORE_LAYOUT", + 262144: "FLOATING", + 524288: "SEND_DRAW_TASK_EVENTS", + 1048576: "OVERFLOW_VISIBLE", + 2097152: "EVENT_TRICKLE", + 4194304: "STATE_TRICKLE", + 8388608: "LAYOUT_1", + 16777216: "LAYOUT_2", + 33554432: "WIDGET_1", + 67108864: "WIDGET_2", + 134217728: "USER_1", + 268435456: "USER_2", + 536870912: "USER_3", + 1073741824: "USER_4", +} + +def decode_obj_flags(raw: int) -> list[str]: + """Decode a bitmask of lv_obj_flag_t into a list of flag names.""" + return [name for bit, name in OBJ_FLAG_NAMES.items() if raw & bit] diff --git a/scripts/gdb/scripts/enum_parser.py b/scripts/gdb/scripts/enum_parser.py index d70543c55d..2ffc5bf23b 100644 --- a/scripts/gdb/scripts/enum_parser.py +++ b/scripts/gdb/scripts/enum_parser.py @@ -68,6 +68,61 @@ def parse_enum(path: Path, enum_type: str, prefix: str, return entries +def parse_bitmask_enum(path: Path, enum_type: str, prefix: str, + skip: set[str] | None = None) -> dict[int, str]: + """Parse a C typedef enum with bitmask values (1u << N) from a header. + + Only entries with explicit ``(1u << N)`` assignments are collected. + Entries whose value references other enum members (aliases / combos) + are silently skipped. + + Args: + path: Path to the C header file. + enum_type: The typedef name (e.g. "lv_obj_flag_t"). + prefix: Enum member prefix to strip (e.g. "LV_OBJ_FLAG_"). + skip: Optional set of full enum member names to skip. + + Returns: + Dict mapping int value -> short name string. + """ + text = path.read_text() + skip = skip or set() + + pattern = rf"\}}\s*{re.escape(enum_type)}\s*;" + m = re.search(rf"typedef\s+enum\s*\{{(.*?){pattern}", text, re.DOTALL) + if not m: + raise RuntimeError(f"Cannot find {enum_type} enum in {path}") + + entries = {} + for line in m.group(1).splitlines(): + line = line.strip().rstrip(",") + if ( + not line + or line.startswith("/*") + or line.startswith("//") + or line.startswith("*") + or line.startswith("#") + ): + continue + + # Match: NAME = (1u << N) or NAME = (1 << N) + match = re.match( + rf"({re.escape(prefix)}\w+)\s*=\s*\(1u?\s*<<\s*(\d+)\)", line + ) + if not match: + continue + + name = match.group(1) + if name in skip: + continue + + bit = int(match.group(2)) + short = name.removeprefix(prefix) + entries[1 << bit] = short + + return entries + + def generate_dict_module( description: str, dicts: dict[str, dict], diff --git a/scripts/gdb/scripts/generators/gen_obj_flag_consts.py b/scripts/gdb/scripts/generators/gen_obj_flag_consts.py new file mode 100644 index 0000000000..42f1c964d1 --- /dev/null +++ b/scripts/gdb/scripts/generators/gen_obj_flag_consts.py @@ -0,0 +1,36 @@ +#!/usr/bin/env python3 +"""Generate object flag constant tables from LVGL headers.""" + +import sys +from pathlib import Path + +sys.path.insert(0, str(Path(__file__).resolve().parent.parent)) +from enum_parser import parse_bitmask_enum, generate_dict_module + +LVGL_SRC = Path(__file__).parent.parent.parent.parent.parent / "src" +OUTPUT = Path(__file__).parent.parent.parent / "lvglgdb" / "lvgl" / "core" / "lv_obj_flag_consts.py" + + +def main(): + obj_flags = parse_bitmask_enum( + LVGL_SRC / "core" / "lv_obj.h", + "lv_obj_flag_t", + "LV_OBJ_FLAG_", + ) + + # Add decode helper after the dict + src = generate_dict_module( + "object flag constants from LVGL headers", + {"OBJ_FLAG_NAMES": obj_flags}, + ) + src += ( + "\ndef decode_obj_flags(raw: int) -> list[str]:\n" + ' """Decode a bitmask of lv_obj_flag_t into a list of flag names."""\n' + " return [name for bit, name in OBJ_FLAG_NAMES.items() if raw & bit]\n" + ) + OUTPUT.write_text(src) + print(f"Generated {OUTPUT.name} ({len(obj_flags)} obj flags)") + + +if __name__ == "__main__": + main()