mirror of
https://github.com/lvgl/lvgl.git
synced 2026-09-24 07:24:57 +08:00
chore(gdb): add widget_specs JSON and spec-driven dashboard rendering
- Add widget_specs.json with auto-generated field types and hand-written summary templates, primary fields, and enum name mappings - Generator merges _auto section with hand-written overrides on each run - data_collector outputs widget_specs to dashboard JSON top-level - Update uinspy frontend with spec-driven detail panel and tree summary
This commit is contained in:
@@ -58,6 +58,8 @@ def collect_all() -> dict:
|
||||
# Registry-driven simple collectors
|
||||
for dict_key, accessor, label in SIMPLE_REGISTRY:
|
||||
data[dict_key] = _collect_simple(lvgl, dict_key, accessor, label)
|
||||
# Widget specs (global lookup table for uinspy rendering)
|
||||
data["widget_specs"] = _collect_widget_specs(data.get("object_trees", []))
|
||||
return data
|
||||
|
||||
|
||||
@@ -220,3 +222,56 @@ def _collect_subjects_from_obj(obj, seen, result):
|
||||
except CorruptedError:
|
||||
# Children pointer unreadable: stop traversal for this subtree
|
||||
pass
|
||||
|
||||
|
||||
def _collect_widget_specs(object_trees: list) -> dict:
|
||||
"""Build widget_specs dict for all class_names found in object trees."""
|
||||
import json as _json
|
||||
from pathlib import Path
|
||||
|
||||
specs_path = Path(__file__).parent.parent.parent.parent / "scripts" / "generators" / "widget_specs.json"
|
||||
if not specs_path.exists():
|
||||
return {}
|
||||
|
||||
all_specs = _json.loads(specs_path.read_text())
|
||||
|
||||
# Collect all class_names from object trees
|
||||
seen = set()
|
||||
|
||||
def _walk(node):
|
||||
cn = node.get("class_name", "")
|
||||
if cn:
|
||||
seen.add(cn)
|
||||
for child in node.get("children", []):
|
||||
_walk(child)
|
||||
|
||||
for tree in object_trees:
|
||||
for screen in tree.get("screens", []):
|
||||
_walk(screen)
|
||||
|
||||
# Build output: merge _auto + enums into flat fields spec
|
||||
result = {}
|
||||
for cn in seen:
|
||||
raw = all_specs.get(cn) or all_specs.get("lv_" + cn)
|
||||
if not raw:
|
||||
continue
|
||||
auto_fields = raw.get("_auto", {}).get("fields", {})
|
||||
enums = raw.get("enums", {})
|
||||
fields = {}
|
||||
for fname, ftype in auto_fields.items():
|
||||
entry = {"type": ftype}
|
||||
if fname in enums:
|
||||
entry["type"] = "enum"
|
||||
entry["names"] = enums[fname]
|
||||
fields[fname] = entry
|
||||
|
||||
spec = {}
|
||||
if raw.get("summary_tpl") is not None:
|
||||
spec["summary_tpl"] = raw["summary_tpl"]
|
||||
if raw.get("primary"):
|
||||
spec["primary"] = raw["primary"]
|
||||
if fields:
|
||||
spec["fields"] = fields
|
||||
if spec:
|
||||
result[cn] = spec
|
||||
return result
|
||||
|
||||
@@ -1137,6 +1137,10 @@ ui-topbar .topbar-search::placeholder {
|
||||
.obj-node > summary:hover {
|
||||
background-color: var(--color-hover-summary);
|
||||
}
|
||||
.obj-summary-hint {
|
||||
margin-left: calc(var(--spacing) * 1);
|
||||
color: var(--color-overlay0);
|
||||
}
|
||||
.obj-node.obj-selected > summary {
|
||||
background-color: var(--color-nav-active-bg);
|
||||
outline: 1px solid var(--blue);
|
||||
@@ -1815,7 +1819,27 @@ function selectObj(addr) {
|
||||
document.getElementById("obj-" + prev)?.classList.remove("obj-selected");
|
||||
selectedAddr.val = addr, document.getElementById("obj-" + addr)?.classList.add("obj-selected");
|
||||
}
|
||||
var dashData = signal(null);
|
||||
var dashData = signal(null), widgetSpecs = {};
|
||||
function setWidgetSpecs(specs) {
|
||||
widgetSpecs = specs;
|
||||
}
|
||||
function getWidgetSpec(className) {
|
||||
return widgetSpecs[className] || widgetSpecs["lv_" + className];
|
||||
}
|
||||
function widgetSummary(className, wd) {
|
||||
if (!wd)
|
||||
return "";
|
||||
let spec = getWidgetSpec(className);
|
||||
if (!spec?.summary_tpl)
|
||||
return "";
|
||||
return spec.summary_tpl.replace(/\{(\w+)\}/g, (_, k) => {
|
||||
let v = wd[k];
|
||||
if (v == null)
|
||||
return "";
|
||||
let s = String(v);
|
||||
return s.length > 24 ? s.slice(0, 24) + "…" : s;
|
||||
});
|
||||
}
|
||||
function countObjects(trees) {
|
||||
let n = 0;
|
||||
function walk(obj) {
|
||||
@@ -2100,9 +2124,17 @@ function buildCard(item, config) {
|
||||
}
|
||||
|
||||
// src/builders/obj-tree.ui.ts
|
||||
function formatWdVal(v) {
|
||||
function formatField(v, spec) {
|
||||
if (v == null)
|
||||
return "-";
|
||||
if (spec) {
|
||||
if (spec.type === "enum" && spec.names && typeof v === "number")
|
||||
return spec.names[v] ?? String(v);
|
||||
if (spec.type === "bool")
|
||||
return v ? "true" : "false";
|
||||
if (spec.type === "string" && typeof v === "string")
|
||||
return v.length > 60 ? '"' + v.slice(0, 60) + '…"' : '"' + v + '"';
|
||||
}
|
||||
if (typeof v === "object")
|
||||
return JSON.stringify(v);
|
||||
return String(v);
|
||||
@@ -2112,7 +2144,15 @@ 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.name ? obj.name + " " : "") + (obj.class_name || "obj"), obj.flags_list?.includes("HIDDEN"))
|
||||
sum.style.setProperty("--depth-color", DEPTH_COLORS[depth % DEPTH_COLORS.length]);
|
||||
let nameText = (obj.name ? obj.name + " " : "") + (obj.class_name || "obj");
|
||||
sum.textContent = nameText;
|
||||
let hint = widgetSummary(obj.class_name, obj.widget_data);
|
||||
if (hint) {
|
||||
let span = document.createElement("span");
|
||||
span.className = "obj-summary-hint", span.textContent = hint, sum.appendChild(span);
|
||||
}
|
||||
if (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) => {
|
||||
@@ -2187,23 +2227,25 @@ function renderObjDetail(addr, panel) {
|
||||
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) {
|
||||
refSec.appendChild(kvPair("children", String(obj.child_count || 0))), refSec.appendChild(kvPair("styles", String(obj.style_count || 0))), panel.appendChild(refSec);
|
||||
let _n = (v) => v != null;
|
||||
if (obj.scroll || _n(obj.ext_click_pad) || _n(obj.ext_draw_size) || _n(obj.scrollbar_mode) || _n(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)
|
||||
if (_n(obj.ext_click_pad))
|
||||
layoutSec.appendChild(kvPair("ext_click_pad", String(obj.ext_click_pad)));
|
||||
if (obj.ext_draw_size)
|
||||
if (_n(obj.ext_draw_size))
|
||||
layoutSec.appendChild(kvPair("ext_draw_size", String(obj.ext_draw_size)));
|
||||
if (obj.scrollbar_mode != null)
|
||||
if (_n(obj.scrollbar_mode))
|
||||
layoutSec.appendChild(kvPair("scrollbar_mode", String(obj.scrollbar_mode)));
|
||||
if (obj.scroll_dir != null)
|
||||
if (_n(obj.scroll_dir))
|
||||
layoutSec.appendChild(kvPair("scroll_dir", String(obj.scroll_dir)));
|
||||
if (obj.scroll_snap_x)
|
||||
if (_n(obj.scroll_snap_x))
|
||||
layoutSec.appendChild(kvPair("scroll_snap_x", String(obj.scroll_snap_x)));
|
||||
if (obj.scroll_snap_y)
|
||||
if (_n(obj.scroll_snap_y))
|
||||
layoutSec.appendChild(kvPair("scroll_snap_y", String(obj.scroll_snap_y)));
|
||||
if (obj.layer_type)
|
||||
if (_n(obj.layer_type))
|
||||
layoutSec.appendChild(kvPair("layer_type", String(obj.layer_type)));
|
||||
if (obj.w_layout)
|
||||
layoutSec.appendChild(kvPair("w_layout", "true"));
|
||||
@@ -2226,38 +2268,23 @@ function renderObjDetail(addr, panel) {
|
||||
intSec.appendChild(wrap), panel.appendChild(intSec);
|
||||
}
|
||||
if (obj.widget_data && Object.keys(obj.widget_data).length) {
|
||||
let wd = obj.widget_data, primary = {
|
||||
lv_label: ["text", "long_mode", "recolor"],
|
||||
lv_image: ["src", "w", "h", "rotation", "scale_x", "scale_y", "align"],
|
||||
lv_bar: ["cur_value", "min_value", "max_value", "start_value", "mode"],
|
||||
lv_slider: ["cur_value", "min_value", "max_value", "start_value", "mode", "dragging"],
|
||||
lv_arc: ["value", "min_value", "max_value", "rotation", "type"],
|
||||
lv_switch: ["anim_state", "orientation"],
|
||||
lv_checkbox: ["txt"],
|
||||
lv_dropdown: ["options", "option_cnt", "sel_opt_id", "dir"],
|
||||
lv_textarea: ["placeholder_txt", "max_length", "pwd_show_time"],
|
||||
lv_tabview: ["tab_cur", "tab_pos", "tab_bar_size"],
|
||||
lv_roller: ["option_cnt", "sel_opt_id", "mode"],
|
||||
lv_chart: ["point_cnt", "hdiv_cnt", "vdiv_cnt", "type"],
|
||||
lv_scale: ["mode", "range_min", "range_max", "total_tick_count", "angle_range", "rotation"],
|
||||
lv_spinner: ["duration", "angle"],
|
||||
lv_keyboard: ["mode", "popovers"],
|
||||
lv_led: ["color", "bright"],
|
||||
lv_spinbox: ["value", "range_min", "range_max", "step", "digit_count", "dec_point_pos"],
|
||||
lv_calendar: ["today", "showed_date"],
|
||||
lv_table: ["col_cnt", "row_cnt"],
|
||||
lv_buttonmatrix: ["btn_cnt", "row_cnt", "btn_id_sel", "one_check"]
|
||||
}[obj.class_name] || [], allKeys = Object.keys(wd), priKeys = allKeys.filter((k) => primary.includes(k)), advKeys = allKeys.filter((k) => !primary.includes(k)), wdSec = el("div", "detail-section");
|
||||
let wd = obj.widget_data, spec = getWidgetSpec(obj.class_name), fieldSpecs = spec?.fields || {}, primary = spec?.primary || [], allKeys = Object.keys(wd), priKeys = primary.filter((k) => (k in wd)), advKeys = allKeys.filter((k) => !priKeys.includes(k)), wdSec = el("div", "detail-section");
|
||||
wdSec.appendChild(el("div", "detail-section-title", "Widget · " + obj.class_name));
|
||||
let renderField = (k) => {
|
||||
let fs = fieldSpecs[k], raw = wd[k];
|
||||
wdSec.appendChild(kvPair(k, formatField(raw, fs)));
|
||||
};
|
||||
for (let k of priKeys.length ? priKeys : allKeys.slice(0, 6))
|
||||
wdSec.appendChild(kvPair(k, formatWdVal(wd[k])));
|
||||
renderField(k);
|
||||
let rest = priKeys.length ? advKeys : allKeys.slice(6);
|
||||
if (rest.length) {
|
||||
let toggle = el("details", "detail-adv-toggle");
|
||||
toggle.appendChild(el("summary", "detail-adv-summary", `${rest.length} more fields`));
|
||||
let inner = el("div", "");
|
||||
for (let k of rest)
|
||||
inner.appendChild(kvPair(k, formatWdVal(wd[k])));
|
||||
for (let k of rest) {
|
||||
let fs = fieldSpecs[k];
|
||||
inner.appendChild(kvPair(k, formatField(wd[k], fs)));
|
||||
}
|
||||
toggle.appendChild(inner), wdSec.appendChild(toggle);
|
||||
}
|
||||
panel.appendChild(wdSec);
|
||||
@@ -3223,7 +3250,7 @@ class UiDashboard extends BaseComponent {
|
||||
let data = dashData.val;
|
||||
if (!data)
|
||||
return;
|
||||
grid.innerHTML = "";
|
||||
setWidgetSpecs(data.widget_specs || {}), grid.innerHTML = "";
|
||||
let bento = el("div", "bento"), objCount = countObjects(data.object_trees || []);
|
||||
STAT_DEFS.forEach((s) => {
|
||||
let val = s.key === "_objects" ? objCount : data[s.key]?.length || 0;
|
||||
@@ -3235,7 +3262,7 @@ class UiDashboard extends BaseComponent {
|
||||
customElements.define("ui-dashboard", UiDashboard);
|
||||
|
||||
// src/app.ts
|
||||
document.getElementById("about").innerHTML = `<span>uinspy v${"0.4.6"}</span><span>·</span><span>Built ${"2026-04-02 13:52 GMT+8"}</span><span>·</span><span>${"07131c2"}</span><span>·</span><span>${"Canvas2D"}</span><span>·</span><a href="https://github.com/W-Mai/uinspy" target="_blank" rel="noopener">GitHub</a><span>·</span><a href="https://lvgl.io" target="_blank" rel="noopener">LVGL</a><span>·</span><span>MIT</span>`;
|
||||
document.getElementById("about").innerHTML = `<span>uinspy v${"0.4.6"}</span><span>·</span><span>Built ${"2026-04-21 19:26 GMT+8"}</span><span>·</span><span>${"1b5dc67"}</span><span>·</span><span>${"Canvas2D"}</span><span>·</span><a href="https://github.com/W-Mai/uinspy" target="_blank" rel="noopener">GitHub</a><span>·</span><a href="https://lvgl.io" target="_blank" rel="noopener">LVGL</a><span>·</span><span>MIT</span>`;
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
@@ -19,6 +19,7 @@ Usage (from the GDB script root):
|
||||
|
||||
import re
|
||||
import sys
|
||||
import json
|
||||
from pathlib import Path
|
||||
from dataclasses import dataclass, field as dc_field
|
||||
|
||||
@@ -28,6 +29,7 @@ LVGL_SRC = Path(__file__).parent.parent.parent.parent.parent / "src"
|
||||
LVGL_INC = Path(__file__).parent.parent.parent.parent.parent / "include" / "lvgl"
|
||||
WIDGETS_DIR = LVGL_SRC / "widgets"
|
||||
OUTPUT_DIR = Path(__file__).parent.parent.parent / "lvglgdb" / "lvgl" / "widgets"
|
||||
SPECS_YAML = Path(__file__).parent / "widget_specs.yaml"
|
||||
|
||||
SIMPLE_INT_TYPES = {
|
||||
"int8_t", "int16_t", "int32_t", "int64_t",
|
||||
@@ -268,6 +270,33 @@ def _field_expr(f: StructField) -> str | None:
|
||||
return None
|
||||
|
||||
|
||||
def _field_type_name(f: StructField) -> str | None:
|
||||
"""Infer a spec type name for a field. Returns None to skip."""
|
||||
if f.is_array:
|
||||
return None
|
||||
if f.is_obj_pointer:
|
||||
return "pointer"
|
||||
if f.is_string:
|
||||
return "string"
|
||||
if f.is_pointer:
|
||||
return "pointer"
|
||||
if f.c_type == "lv_color_t":
|
||||
return "color"
|
||||
if f.c_type == "lv_area_t":
|
||||
return "area"
|
||||
if f.c_type == "lv_point_t":
|
||||
return "point"
|
||||
if f.is_bitfield:
|
||||
return "bool" if f.bitfield_width == 1 else f"enum:{f.bitfield_width}"
|
||||
if f.c_type == "bool":
|
||||
return "bool"
|
||||
if f.c_type in SIMPLE_INT_TYPES or f.c_type.startswith(("uint", "int")):
|
||||
return "int"
|
||||
if f.c_type.startswith("lv_") and f.c_type.endswith("_t"):
|
||||
return "int"
|
||||
return None
|
||||
|
||||
|
||||
def _topo_sort(widgets: dict[str, WidgetDef]) -> list[WidgetDef]:
|
||||
result, visited = [], set()
|
||||
def visit(w):
|
||||
@@ -496,6 +525,61 @@ def gen_init(ordered: list[WidgetDef]) -> str:
|
||||
return "\n".join(lines)
|
||||
|
||||
|
||||
SPECS_JSON = Path(__file__).parent / "widget_specs.json"
|
||||
|
||||
|
||||
def _build_auto_spec(wdef: WidgetDef) -> dict:
|
||||
"""Build the _auto section for a widget from parsed fields."""
|
||||
fields = {}
|
||||
for f in wdef.fields:
|
||||
t = _field_type_name(f)
|
||||
if t:
|
||||
fields[f.name] = t
|
||||
return {"fields": fields}
|
||||
|
||||
|
||||
def update_specs_json(widgets: dict[str, WidgetDef]) -> dict:
|
||||
"""Merge auto-generated field info with hand-written specs.
|
||||
|
||||
- _auto section is always regenerated
|
||||
- Hand-written keys (summary_tpl, primary, enums) are preserved
|
||||
- New widgets get an empty template
|
||||
- Removed widgets get _removed: true
|
||||
"""
|
||||
existing = {}
|
||||
if SPECS_JSON.exists():
|
||||
existing = json.loads(SPECS_JSON.read_text())
|
||||
|
||||
result = {}
|
||||
# Preserve _comment
|
||||
if "_comment" in existing:
|
||||
result["_comment"] = existing["_comment"]
|
||||
else:
|
||||
result["_comment"] = "Auto-generated + hand-written widget specs. _auto is regenerated; other keys are preserved."
|
||||
|
||||
seen = set()
|
||||
for wdef in sorted(widgets.values(), key=lambda w: w.c_class_name):
|
||||
key = wdef.c_class_name # e.g. "lv_label"
|
||||
seen.add(key)
|
||||
old = existing.get(key, {})
|
||||
entry = {"_auto": _build_auto_spec(wdef)}
|
||||
# Preserve hand-written keys
|
||||
for k in ("summary_tpl", "primary", "enums"):
|
||||
if k in old:
|
||||
entry[k] = old[k]
|
||||
result[key] = entry
|
||||
|
||||
# Mark removed widgets
|
||||
for key, val in existing.items():
|
||||
if key.startswith("_") or key in seen:
|
||||
continue
|
||||
val["_removed"] = True
|
||||
result[key] = val
|
||||
|
||||
SPECS_JSON.write_text(json.dumps(result, indent=2, ensure_ascii=False) + "\n")
|
||||
return result
|
||||
|
||||
|
||||
def main():
|
||||
widgets = parse_widgets()
|
||||
ordered = _topo_sort(widgets)
|
||||
@@ -504,6 +588,11 @@ def main():
|
||||
for w in ordered:
|
||||
print(f" {w.module_name}.py: {w.class_name}({w.parent_class_name}) — {len(w.fields)} fields")
|
||||
|
||||
# Update specs JSON (merge auto + hand-written)
|
||||
specs = update_specs_json(widgets)
|
||||
new_count = sum(1 for k, v in specs.items() if not k.startswith("_") and "summary_tpl" not in v)
|
||||
print(f"\nUpdated {SPECS_JSON.name}: {len(specs) - 1} widgets ({new_count} need manual spec)")
|
||||
|
||||
OUTPUT_DIR.mkdir(parents=True, exist_ok=True)
|
||||
|
||||
# _helpers.py
|
||||
@@ -517,7 +606,7 @@ def main():
|
||||
# __init__.py
|
||||
(OUTPUT_DIR / "__init__.py").write_text(gen_init(ordered))
|
||||
|
||||
print(f"\nGenerated {len(ordered) + 2} files in {OUTPUT_DIR}/")
|
||||
print(f"Generated {len(ordered) + 2} files in {OUTPUT_DIR}/")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user