diff --git a/scripts/gdb/lvglgdb/__init__.py b/scripts/gdb/lvglgdb/__init__.py index 8eb6094483..84b9bc6b8d 100644 --- a/scripts/gdb/lvglgdb/__init__.py +++ b/scripts/gdb/lvglgdb/__init__.py @@ -46,6 +46,8 @@ from .lvgl import ( LVObjClass, OBJ_FLAG_NAMES, decode_obj_flags, + OBJ_STATE_NAMES, + decode_obj_states, LVSubject, LVObserver, SUBJECT_TYPE_NAMES, @@ -99,6 +101,8 @@ __all__ = [ "LVObjClass", "OBJ_FLAG_NAMES", "decode_obj_flags", + "OBJ_STATE_NAMES", + "decode_obj_states", "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 83bf944289..5bd4e37d6b 100644 --- a/scripts/gdb/lvglgdb/cmds/dashboard/static/uinspy.html +++ b/scripts/gdb/lvglgdb/cmds/dashboard/static/uinspy.html @@ -47,6 +47,7 @@ --color-blue: var(--blue); --color-sapphire: var(--sapphire); --color-green: var(--green); + --color-yellow: var(--yellow); --color-peach: var(--peach); --color-mauve: var(--mauve); --color-teal: var(--teal); @@ -1237,6 +1238,17 @@ ui-topbar .topbar-search::placeholder { color: var(--color-overlay1); border: 1px solid var(--surface0); } +.detail-state-active { + border-color: var(--color-yellow); + @supports (color: color-mix(in lab, red, red)) { + border-color: color-mix(in oklab, var(--color-yellow) 30%, transparent); + } + background-color: var(--color-yellow); + @supports (color: color-mix(in lab, red, red)) { + background-color: color-mix(in oklab, var(--color-yellow) 10%, transparent); + } + color: var(--color-yellow); +} .scene-controls { margin-bottom: calc(var(--spacing) * 1); display: flex; @@ -2075,7 +2087,7 @@ 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", obj.flags_list?.includes("HIDDEN")) + if (sum.style.setProperty("--depth-color", DEPTH_COLORS[depth % DEPTH_COLORS.length]), sum.textContent = (obj.name ? obj.name + " " : "") + (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) => { @@ -2131,6 +2143,12 @@ function renderObjDetail(addr, panel) { 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); } + if (obj.state_list?.length) { + let stateSec = el("div", "detail-section"); + stateSec.appendChild(el("div", "detail-section-title", "State")); + let wrap = el("div", "detail-flags-wrap"); + obj.state_list.forEach((s) => wrap.appendChild(el("span", "detail-flag-badge" + (s === "DEFAULT" ? "" : " detail-state-active"), s))), stateSec.appendChild(wrap), panel.appendChild(stateSec); + } let refSec = el("div", "detail-section"); if (refSec.appendChild(el("div", "detail-section-title", "References")), obj.parent_addr) { let row = el("div", "kv-row"); @@ -2140,7 +2158,49 @@ function renderObjDetail(addr, panel) { let row = el("div", "kv-row"); row.appendChild(el("span", "kv-label", "group")), row.appendChild(xref(obj.group_addr, "group")), refSec.appendChild(row); } - if (refSec.appendChild(kvPair("children", String(obj.child_count || 0))), refSec.appendChild(kvPair("styles", String(obj.style_count || 0))), panel.appendChild(refSec), obj.styles?.length) { + if (obj.user_data) + refSec.appendChild(kvPair("user_data", obj.user_data)); + if (obj.name) + refSec.appendChild(kvPair("name", obj.name)); + if (refSec.appendChild(kvPair("children", String(obj.child_count || 0))), refSec.appendChild(kvPair("styles", String(obj.style_count || 0))), panel.appendChild(refSec), obj.scroll || obj.ext_click_pad || obj.ext_draw_size || obj.scrollbar_mode || obj.layer_type || obj.w_layout || obj.h_layout) { + let layoutSec = el("div", "detail-section"); + if (layoutSec.appendChild(el("div", "detail-section-title", "Layout & Scroll")), obj.scroll) + layoutSec.appendChild(kvPair("scroll", `${obj.scroll.x}, ${obj.scroll.y}`)); + if (obj.ext_click_pad) + layoutSec.appendChild(kvPair("ext_click_pad", String(obj.ext_click_pad))); + if (obj.ext_draw_size) + layoutSec.appendChild(kvPair("ext_draw_size", String(obj.ext_draw_size))); + if (obj.scrollbar_mode != null) + layoutSec.appendChild(kvPair("scrollbar_mode", String(obj.scrollbar_mode))); + if (obj.scroll_dir != null) + layoutSec.appendChild(kvPair("scroll_dir", String(obj.scroll_dir))); + if (obj.scroll_snap_x) + layoutSec.appendChild(kvPair("scroll_snap_x", String(obj.scroll_snap_x))); + if (obj.scroll_snap_y) + layoutSec.appendChild(kvPair("scroll_snap_y", String(obj.scroll_snap_y))); + if (obj.layer_type) + layoutSec.appendChild(kvPair("layer_type", String(obj.layer_type))); + if (obj.w_layout) + layoutSec.appendChild(kvPair("w_layout", "true")); + if (obj.h_layout) + layoutSec.appendChild(kvPair("h_layout", "true")); + panel.appendChild(layoutSec); + } + if (obj.layout_inv || obj.is_deleting || obj.rendered || obj.skip_trans) { + let intSec = el("div", "detail-section"); + intSec.appendChild(el("div", "detail-section-title", "Internal")); + let wrap = el("div", "detail-flags-wrap"); + if (obj.layout_inv) + wrap.appendChild(el("span", "detail-flag-badge detail-state-active", "layout_inv")); + if (obj.is_deleting) + wrap.appendChild(el("span", "detail-flag-badge detail-state-active", "is_deleting")); + if (obj.rendered) + wrap.appendChild(el("span", "detail-flag-badge", "rendered")); + if (obj.skip_trans) + wrap.appendChild(el("span", "detail-flag-badge", "skip_trans")); + intSec.appendChild(wrap), panel.appendChild(intSec); + } + if (obj.styles?.length) { let styleSec = el("div", "detail-section"); styleSec.appendChild(el("div", "detail-section-title", "Styles (" + obj.styles.length + ")")), obj.styles.forEach((s) => { let card = el("div", "detail-style-card"); @@ -3113,7 +3173,7 @@ class UiDashboard extends BaseComponent { customElements.define("ui-dashboard", UiDashboard); // src/app.ts -document.getElementById("about").innerHTML = `uinspy v${"0.4.6"}·Built ${"2026-04-01 13:27 GMT+8"}·${"8f35af9"}·${"Canvas2D"}·GitHub·LVGL·MIT`; +document.getElementById("about").innerHTML = `uinspy v${"0.4.6"}·Built ${"2026-04-02 03:55 GMT+8"}·${"be336ae"}·${"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 6ebb3f3e2c..6d140ff039 100644 --- a/scripts/gdb/lvglgdb/lvgl/__init__.py +++ b/scripts/gdb/lvglgdb/lvgl/__init__.py @@ -10,6 +10,8 @@ from .core import ( LVObjClass, OBJ_FLAG_NAMES, decode_obj_flags, + OBJ_STATE_NAMES, + decode_obj_states, LVSubject, LVObserver, SUBJECT_TYPE_NAMES, @@ -100,6 +102,8 @@ __all__ = [ "LVObjClass", "OBJ_FLAG_NAMES", "decode_obj_flags", + "OBJ_STATE_NAMES", + "decode_obj_states", "LVSubject", "LVObserver", "SUBJECT_TYPE_NAMES", diff --git a/scripts/gdb/lvglgdb/lvgl/core/__init__.py b/scripts/gdb/lvglgdb/lvgl/core/__init__.py index b353bbfce2..3f67fe8c0e 100644 --- a/scripts/gdb/lvglgdb/lvgl/core/__init__.py +++ b/scripts/gdb/lvglgdb/lvgl/core/__init__.py @@ -4,6 +4,7 @@ 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_obj_state_consts import OBJ_STATE_NAMES, decode_obj_states from .lv_observer import LVSubject, LVObserver from .lv_observer_consts import SUBJECT_TYPE_NAMES @@ -19,6 +20,8 @@ __all__ = [ "LVObjClass", "OBJ_FLAG_NAMES", "decode_obj_flags", + "OBJ_STATE_NAMES", + "decode_obj_states", "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 359bef3c72..fc0f59cb13 100644 --- a/scripts/gdb/lvglgdb/lvgl/core/lv_obj.py +++ b/scripts/gdb/lvglgdb/lvgl/core/lv_obj.py @@ -101,6 +101,15 @@ class LVObject(Value): from .lv_obj_flag_consts import decode_obj_flags return decode_obj_flags(self.flags_raw) + @property + def state_raw(self) -> int: + return int(self.safe_field("state", 0)) + + @property + def state_list(self) -> list[str]: + from .lv_obj_state_consts import decode_obj_states + return decode_obj_states(self.state_raw) + @property def child_cnt(self) -> int: """Return child count, 0 if corrupted.""" @@ -167,8 +176,26 @@ class LVObject(Value): ("style_count", lambda s: int(s.style_cnt), 0), ("flags", lambda s: s.flags_raw, 0), ("flags_list", lambda s: s.flags_list, []), + ("state", lambda s: s.state_raw, 0), + ("state_list", lambda s: s.state_list, []), ("parent_addr", lambda s: ptr_or_none(s.super_value("parent"))), ("group_addr", lambda s: s._get_group_addr()), + ("user_data", lambda s: ptr_or_none(s.safe_field("user_data"))), + ("layout_inv", lambda s: bool(s.safe_field("layout_inv", 0, int)), False), + ("w_layout", lambda s: bool(s.safe_field("w_layout", 0, int)), False), + ("h_layout", lambda s: bool(s.safe_field("h_layout", 0, int)), False), + ("is_deleting", lambda s: bool(s.safe_field("is_deleting", 0, int)), False), + ("rendered", lambda s: bool(s.safe_field("rendered", 0, int)), False), + ("skip_trans", lambda s: bool(s.safe_field("skip_trans", 0, int)), False), + ("scroll", lambda s: s._get_scroll()), + ("ext_click_pad", lambda s: s._get_spec_int("ext_click_pad")), + ("ext_draw_size", lambda s: s._get_spec_int("ext_draw_size")), + ("scrollbar_mode", lambda s: s._get_spec_int("scrollbar_mode")), + ("scroll_snap_x", lambda s: s._get_spec_int("scroll_snap_x")), + ("scroll_snap_y", lambda s: s._get_spec_int("scroll_snap_y")), + ("scroll_dir", lambda s: s._get_spec_int("scroll_dir")), + ("layer_type", lambda s: s._get_spec_int("layer_type")), + ("name", lambda s: s._get_name()), ]) if include_children: d["children"] = self._collect_children(include_styles) @@ -211,6 +238,33 @@ class LVObject(Value): addr = int(grp) return hex(addr) if addr else None + def _get_spec_int(self, field_name): + """Get an int field from spec_attr, or None.""" + spec = self.spec_attr + if not spec or not int(spec): + return None + return int(spec.safe_field(field_name, 0)) + + def _get_scroll(self): + """Get scroll offset from spec_attr, or None.""" + spec = self.spec_attr + if not spec or not int(spec): + return None + scroll = spec.safe_field("scroll") + if scroll is None: + return None + return {"x": int(scroll["x"]), "y": int(scroll["y"])} + + def _get_name(self): + """Get object name from spec_attr, or None.""" + spec = self.spec_attr + if not spec or not int(spec): + return None + name = spec.safe_field("name") + if name is None or not int(name): + return None + return name.string() + def dump_obj_info(obj: LVObject): from lvglgdb.lvgl.formatter import print_info diff --git a/scripts/gdb/lvglgdb/lvgl/core/lv_obj_state_consts.py b/scripts/gdb/lvglgdb/lvgl/core/lv_obj_state_consts.py new file mode 100644 index 0000000000..33488b3641 --- /dev/null +++ b/scripts/gdb/lvglgdb/lvgl/core/lv_obj_state_consts.py @@ -0,0 +1,29 @@ +""" +Auto-generated object state constants from LVGL headers. + +Do not edit manually. Regenerate from the GDB script root with: + python3 scripts/generate_all.py +""" + +OBJ_STATE_NAMES = { + 0: "DEFAULT", + 1: "ALT", + 4: "CHECKED", + 8: "FOCUSED", + 16: "FOCUS_KEY", + 32: "EDITED", + 64: "HOVERED", + 128: "PRESSED", + 256: "SCROLLED", + 512: "DISABLED", + 4096: "USER_1", + 8192: "USER_2", + 16384: "USER_3", + 32768: "USER_4", +} + +def decode_obj_states(raw: int) -> list[str]: + """Decode a bitmask of lv_state_t into a list of state names.""" + if raw == 0: + return ["DEFAULT"] + return [name for bit, name in OBJ_STATE_NAMES.items() if bit and raw & bit] diff --git a/scripts/gdb/scripts/enum_parser.py b/scripts/gdb/scripts/enum_parser.py index 2ffc5bf23b..a20ecd5f00 100644 --- a/scripts/gdb/scripts/enum_parser.py +++ b/scripts/gdb/scripts/enum_parser.py @@ -105,9 +105,9 @@ def parse_bitmask_enum(path: Path, enum_type: str, prefix: str, ): continue - # Match: NAME = (1u << N) or NAME = (1 << N) + # Match: NAME = (1u << N) or NAME = 1 << N (parens optional) match = re.match( - rf"({re.escape(prefix)}\w+)\s*=\s*\(1u?\s*<<\s*(\d+)\)", line + rf"({re.escape(prefix)}\w+)\s*=\s*\(?1u?\s*<<\s*(\d+)\)?", line ) if not match: continue diff --git a/scripts/gdb/scripts/generators/gen_obj_state_consts.py b/scripts/gdb/scripts/generators/gen_obj_state_consts.py new file mode 100644 index 0000000000..0905ee340f --- /dev/null +++ b/scripts/gdb/scripts/generators/gen_obj_state_consts.py @@ -0,0 +1,42 @@ +#!/usr/bin/env python3 +"""Generate object state 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_state_consts.py" + +SKIP = {"LV_STATE_ANY"} + + +def main(): + obj_states = parse_bitmask_enum( + LVGL_SRC / "core" / "lv_obj_style.h", + "lv_state_t", + "LV_STATE_", + skip=SKIP, + ) + # DEFAULT = 0 is not a bitmask entry, add manually + obj_states[0] = "DEFAULT" + + src = generate_dict_module( + "object state constants from LVGL headers", + {"OBJ_STATE_NAMES": obj_states}, + ) + src += ( + "\ndef decode_obj_states(raw: int) -> list[str]:\n" + ' """Decode a bitmask of lv_state_t into a list of state names."""\n' + " if raw == 0:\n" + ' return ["DEFAULT"]\n' + " return [name for bit, name in OBJ_STATE_NAMES.items() if bit and raw & bit]\n" + ) + OUTPUT.write_text(src) + print(f"Generated {OUTPUT.name} ({len(obj_states)} obj states)") + + +if __name__ == "__main__": + main()