From 85b6fa62f394467bf66043185b17faeeb63005eb Mon Sep 17 00:00:00 2001 From: Arda Aydin Date: Sat, 3 Oct 2026 23:15:47 +0300 Subject: [PATCH] Drag links out of slots on the wiresheet; hide @palette=false types Hovering a slot row highlights the half under the pointer and shows a direction cursor. Pressing on a slot drags a rubber band; dropping on another slot links them, dropping on a title bar opens the Link dialog with the dragged slot preselected, and a no-entry cursor marks a slot that would refuse the link. A right click that never moves still opens the context menu. The palette now leaves out types whose facets carry @palette=false, and kits with nothing placeable left. Also points the default device host at 192.168.1.251. Co-Authored-By: Claude Opus 5.5 --- app_state.py | 2 +- assets/cursors/no_entry.png | Bin 0 -> 444 bytes editor.properties | 2 +- link_drag.py | 495 ++++++++++++++++++++++++++++++++++++ main.py | 29 ++- palette.py | 20 +- wiresheet.py | 1 + 7 files changed, 538 insertions(+), 11 deletions(-) create mode 100644 assets/cursors/no_entry.png create mode 100644 link_drag.py diff --git a/app_state.py b/app_state.py index 73a0140..24e7b5a 100644 --- a/app_state.py +++ b/app_state.py @@ -8,7 +8,7 @@ so later rebinding would only ever be seen by the module that did it. import configparser import os -VERSION = "0.0.0.138" +VERSION = "0.0.0.139" PROPERTIES_FILE = os.path.join(os.path.dirname(os.path.abspath(__file__)), "editor.properties") DEFAULT_GRID_SIZE = 10 DEFAULT_ADD_TIMESTAMP = True diff --git a/assets/cursors/no_entry.png b/assets/cursors/no_entry.png new file mode 100644 index 0000000000000000000000000000000000000000..fc3345ce97f71285ae8ebbf7cbd4294a9da8bde8 GIT binary patch literal 444 zcmeAS@N?(olHy`uVBq!ia0vp^d?3uh1|;P@bT0xa#^NA%Cx&(BWL^R}Ea{HEjtmSN z`?>!lvI6;>1s;*bKn;gMnDKc2iWHz=T55!6ny0500|!VuLuNY*kYr$x0%8zgW?-1W z&cFg>85kKGFfL#Q%Ysy}E`X?X0kT1W2`Iw^RvBbz0c1gS85$UXWR-p&H{5xJc?Xbr z-_yl0#KM2-BtyO?2LacYUzlPX&O5TtX>9Z6_{b3=@XkSb>-G(ouNf!gCw@AU-gJn` za9_%#vm55m{AoRJJ5y@sp|komrUv5_0_EX)89p?{Asqykgd>_umV4?=({8@x8HI zUHI4>zci2AexIGnpMF~u5VQWvGTHviJJ}yD37x)f_IcYazYNn~N*<|r_xYyb@>OTD zBKNsnK9I)j`fyL`tbcJ0Dl_x@e^fu6_G9al{O!qCo-wU_F9-6rr>mdKI;Vst0B7}> Al>h($ literal 0 HcmV?d00001 diff --git a/editor.properties b/editor.properties index 55385a0..1e923ea 100644 --- a/editor.properties +++ b/editor.properties @@ -7,7 +7,7 @@ left_pane_width = 230 trace_dasp = True [Device] -host = 192.168.1.29 +host = 192.168.1.251 port = 1876 user = admin diff --git a/link_drag.py b/link_drag.py new file mode 100644 index 0000000..7a04aa3 --- /dev/null +++ b/link_drag.py @@ -0,0 +1,495 @@ +"""Linking on the wiresheet with the mouse: slot hover, drag-out rubber band, drop. + +Hovering a slot row highlights the half of it the pointer is in and swaps the cursor for +a horizontal arrow: the right half of a row is where a link leaves a slot, so it points +right, and the left half is where one arrives, so it points left. Pressing either +button on a slot and dragging pulls a rubber-band line out of that side; letting go on +another slot makes the link, and letting go on a component's title bar opens the Link +dialog for the two components with the dragged slot already picked. Started on the right half, the slot under the pointer is +the target; started on the left half, it is the source -- so a link can be drawn from +either end. While dragging, the cursor points along the flow of the link over a slot that +would take it, and turns into a no-entry sign over one that would not, so a bad drop is +visible before the button is released. + +A right press that never moves still opens the component's context menu. A left press +anywhere on a box selects it, but only a press on its title bar moves it; on a slot row +it draws a link instead, as in Workbench. +""" + +import os +import struct +import tempfile +import tkinter as tk + +import app_state +import sax_file +import wiresheet +from sax_dom import get_component_path + +NO_ENTRY_PNG = os.path.join(os.path.dirname(os.path.abspath(__file__)), + "assets", "cursors", "no_entry.png") +DRAG_THRESHOLD = 4 # pixels the pointer must travel before a press is a drag +HOVER_COLOR = "#1f4e79" +DROP_OK_COLOR = "#2e9d3a" +DROP_BAD_COLOR = "#d62828" +RUBBER_COLOR = "#2f6fba" + +_cursors = {} # role -> Tk cursor spec, built on first use +_press = None # pending right press: {"hit", "x", "y", "event"} +_drag = None # live link drag: {"hit", "anchor"} +_menu_callback = None # opens the ordinary right-click menu for an event +_link_dialog = None # opens the Link dialog: (source, target, src_slot, tgt_slot) + + +# --------------------------------------------------------------------------- cursors + +def _write_cur(path, pixels, hotspot): + """Writes a 32x32 32-bit Windows .cur from rows of (r, g, b, a) tuples.""" + size = 32 + xor = bytearray() + for row in reversed(pixels): # DIBs are stored bottom-up + for r, g, b, a in row: + xor += bytes((b, g, r, a)) + mask = bytearray() + for row in reversed(pixels): + bits = 0 + for x, (_, _, _, a) in enumerate(row): + if a == 0: + bits |= 1 << (31 - x) + mask += struct.pack(">I", bits) + header = struct.pack(" py) != (yj > py) and px < (xj - xi) * (py - yi) / (yj - yi) + xi: + inside = not inside + j = i + return inside + + +def _arrow_pixels(pointing_right): + """A hollow block arrow: white body, black rim -- the Workbench link cursor.""" + poly = [(9, 14), (16, 14), (16, 10), (23, 16), (16, 22), (16, 18), (9, 18)] + if not pointing_right: + poly = [(32 - x, y) for x, y in poly] + inside = [[_inside_polygon(x + 0.5, y + 0.5, poly) for x in range(32)] + for y in range(32)] + rows = [] + for y in range(32): + row = [] + for x in range(32): + if not inside[y][x]: + row.append((0, 0, 0, 0)) + continue + rim = any(not (0 <= x + dx < 32 and 0 <= y + dy < 32 and inside[y + dy][x + dx]) + for dx in (-1, 0, 1) for dy in (-1, 0, 1)) + row.append((0, 0, 0, 255) if rim else (255, 255, 255, 255)) + rows.append(row) + return rows + + +def _no_entry_pixels(): + """assets/cursors/no_entry.png, centred on a 32x32 cursor; None if unreadable. + + Read with PIL when it is installed, which keeps the image's soft edges; otherwise + with Tk's own PNG reader, which only knows whether a pixel is transparent or not. + """ + if not os.path.exists(NO_ENTRY_PNG): + return None + try: + from PIL import Image + image = Image.open(NO_ENTRY_PNG).convert("RGBA") + width, height = image.size + + def pixel(x, y): + return image.getpixel((x, y)) + except ImportError: + image = tk.PhotoImage(file=NO_ENTRY_PNG) + width, height = image.width(), image.height() + + def pixel(x, y): + if image.transparency_get(x, y): + return (0, 0, 0, 0) + return tuple(image.get(x, y)) + (255,) + if width > 32 or height > 32: + return None + left, top = 16 - width // 2, 16 - height // 2 + rows = [[(0, 0, 0, 0)] * 32 for _ in range(32)] + for y in range(height): + for x in range(width): + rows[top + y][left + x] = pixel(x, y) + return rows + + +def _cursor(role): + """The Tk cursor for 'right', 'left' or 'no', drawn once and cached.""" + if role in _cursors: + return _cursors[role] + fallback = {"right": "sb_right_arrow", "left": "sb_left_arrow", "no": "X_cursor"}[role] + spec = fallback + if os.name == "nt": + try: + folder = os.path.join(tempfile.gettempdir(), "sedona_editor_cursors") + os.makedirs(folder, exist_ok=True) + path = os.path.join(folder, f"link_{role}.cur") + if role == "right": + _write_cur(path, _arrow_pixels(True), (23, 16)) + elif role == "left": + _write_cur(path, _arrow_pixels(False), (9, 16)) + else: + pixels = _no_entry_pixels() + if pixels is None: + raise OSError("no_entry.png missing or unreadable") + _write_cur(path, pixels, (16, 16)) + candidate = "{@" + path.replace("\\", "/") + "}" + app_state.canvas.configure(cursor=candidate) # proves Tk can load it + spec = candidate + except (OSError, tk.TclError): + spec = "no" if role == "no" else fallback + _cursors[role] = spec + return spec + + +def _set_cursor(role): + canvas = app_state.canvas + try: + canvas.configure(cursor=_cursor(role) if role else "") + except tk.TclError: + canvas.configure(cursor="") + + +# --------------------------------------------------------------------------- hit test + +def slot_at(cx, cy): + """The slot row under a canvas point, as a dict, or None. + + Picks the topmost box under the point, so a box lying over another wins, and says + which half of the row the point is in. + """ + canvas = app_state.canvas + for item in reversed(canvas.find_overlapping(cx, cy, cx, cy)): + tags = canvas.gettags(item) + if "component" not in tags: + continue + comp_tag = next((t for t in tags if t.startswith("comp_group_")), None) + if comp_tag is None: + continue + box = wiresheet.get_box_bounds(comp_tag) + if box is None: + return None + zoom = app_state.zoom + half_row = wiresheet.SLOT_ROW_HEIGHT * zoom / 2 + for slot_name, offset in app_state.component_slot_offsets.get(comp_tag, {}).items(): + row_y = box[1] + offset * zoom + if row_y - half_row <= cy < row_y + half_row: + return { + "comp_tag": comp_tag, + "comp": app_state.canvas_comp_map.get(comp_tag), + "slot": slot_name, + "side": "right" if cx >= (box[0] + box[2]) / 2 else "left", + "box": box, + "row_y": row_y, + "half_row": half_row, + } + return None # over a header or footer, not a slot + return None + + +def title_at(cx, cy): + """The comp tag whose title bar is topmost under a canvas point, or None.""" + canvas = app_state.canvas + for item in reversed(canvas.find_overlapping(cx, cy, cx, cy)): + tags = canvas.gettags(item) + if "component" not in tags: + continue + comp_tag = next((t for t in tags if t.startswith("comp_group_")), None) + if comp_tag is None: + continue + box = wiresheet.get_box_bounds(comp_tag) + if box and box[1] <= cy <= box[1] + wiresheet.HEADER_HEIGHT * app_state.zoom: + return comp_tag + return None + return None + + +def _slot_info(comp, slot_name): + for slot in app_state.final_components_registry.get(comp.get("type", ""), []): + if slot["name"] == slot_name: + return slot + return None + + +def _check_link(source, target): + """Returns (ok, reason) for a link from one hit slot to another.""" + if source["comp"] is None or target["comp"] is None: + return False, "No component there." + if source["comp"] is target["comp"] and source["slot"] == target["slot"]: + return False, "A slot cannot be linked to itself." + src = _slot_info(source["comp"], source["slot"]) + tgt = _slot_info(target["comp"], target["slot"]) + if src is None or tgt is None: + return False, "Slot not found in the kit manifest." + for slot in (src, tgt): + if "a" in slot["flags"] or slot["type"] == "void": + return False, f"'{slot['name']}' is an action and cannot be linked." + if tgt["readonly"]: + return False, f"'{tgt['name']}' is read-only and cannot be a link target." + if src["type"] != tgt["type"]: + return False, (f"Type mismatch: {source['slot']} is {src['type']}, " + f"{target['slot']} is {tgt['type']}.") + from_ref, to_ref = _refs(source, target) + links = app_state.xml_root_element.find("links") + if links is not None: + for link in links.findall("link"): + if link.get("from") == from_ref and link.get("to") == to_ref: + return False, "That link already exists." + return True, f"Link {from_ref} → {to_ref}" + + +def _refs(source, target): + root = app_state.xml_root_element + return (f"{get_component_path(source['comp'], root)}.{source['slot']}", + f"{get_component_path(target['comp'], root)}.{target['slot']}") + + +# --------------------------------------------------------------------------- drawing + +def _highlight(hit, color): + """Rings the half of a slot row the pointer is over, with a bar on its outer edge.""" + canvas = app_state.canvas + canvas.delete("slot_hover") + if hit is None: + return + left, _, right, _ = hit["box"] + mid = (left + right) / 2 + top, bottom = hit["row_y"] - hit["half_row"], hit["row_y"] + hit["half_row"] + x1, x2 = (mid, right - 1) if hit["side"] == "right" else (left + 1, mid) + canvas.create_rectangle(x1, top, x2, bottom, outline=color, width=2, + tags=("slot_hover",)) + edge = right - 2 if hit["side"] == "right" else left + 2 + canvas.create_line(edge, top, edge, bottom, fill=color, width=4, tags=("slot_hover",)) + canvas.tag_raise("slot_hover") + + +def _highlight_title(comp_tag, color): + """Rings a box's title bar: the drop that opens the Link dialog for that box.""" + canvas = app_state.canvas + canvas.delete("slot_hover") + left, top, right, _ = wiresheet.get_box_bounds(comp_tag) + canvas.create_rectangle(left + 1, top + 1, right - 1, + top + wiresheet.HEADER_HEIGHT * app_state.zoom, + outline=color, width=2, tags=("slot_hover",)) + canvas.tag_raise("slot_hover") + + +def _anchor(hit): + left, _, right, _ = hit["box"] + return (right if hit["side"] == "right" else left), hit["row_y"] + + +def clear(): + """Drops any hover ring, rubber band and drag state; called on every repaint.""" + global _press, _drag + canvas = app_state.canvas + if canvas is None: + return + canvas.delete("slot_hover") + canvas.delete("link_rubber") + _press = None + _drag = None + _set_cursor(None) + + +# --------------------------------------------------------------------------- events + +def on_motion(event): + """Plain pointer movement: ring the slot half under the pointer.""" + if _drag is not None or app_state.drag_data.get("tag"): + return + canvas = app_state.canvas + hit = slot_at(canvas.canvasx(event.x), canvas.canvasy(event.y)) + _highlight(hit, HOVER_COLOR) + _set_cursor(hit["side"] if hit else None) + + +def on_leave(_event): + if _drag is None: + app_state.canvas.delete("slot_hover") + _set_cursor(None) + + +def _start_press(event): + """Records a press on a slot row as a link drag in waiting; None if not on a slot.""" + global _press + canvas = app_state.canvas + hit = slot_at(canvas.canvasx(event.x), canvas.canvasy(event.y)) + if hit is None or hit["comp"] is None: + _press = None + return None + _press = {"hit": hit, "x": event.x, "y": event.y} + return _press + + +def _drag_motion(event): + """Pulls the rubber band to the pointer and judges the slot under it, if any.""" + global _drag + canvas = app_state.canvas + if _drag is None: + if _press is None: + return + if abs(event.x - _press["x"]) < DRAG_THRESHOLD and abs(event.y - _press["y"]) < DRAG_THRESHOLD: + return + _drag = {"hit": _press["hit"], "anchor": _anchor(_press["hit"])} + app_state.set_status("Drop on a slot to link it; release elsewhere to cancel.") + + cx, cy = canvas.canvasx(event.x), canvas.canvasy(event.y) + start = _drag["hit"] + ax, ay = _drag["anchor"] + canvas.delete("link_rubber") + canvas.create_line(ax, ay, cx, cy, fill=RUBBER_COLOR, width=2, dash=(4, 2), + tags=("link_rubber",)) + canvas.tag_raise("link_rubber") + + outgoing = start["side"] == "right" + hit = slot_at(cx, cy) + if hit is None: + title_tag = title_at(cx, cy) + if title_tag is not None: + # Its own title too: a component may link one of its slots to another. + _highlight_title(title_tag, DROP_OK_COLOR) + _set_cursor("right" if outgoing else "left") + app_state.set_status("Drop to pick the other slot in the Link dialog.") + return + if hit is None or (hit["comp_tag"] == start["comp_tag"] and hit["slot"] == start["slot"]): + canvas.delete("slot_hover") + _set_cursor(None) + return + ok, reason = _check_link(start, hit) if outgoing else _check_link(hit, start) + # Ring the side of the drop slot the link would attach to, not the side under the + # pointer: a link always arrives on the left of a row and leaves from its right. + shown = dict(hit, side="left" if outgoing else "right") + _highlight(shown, DROP_OK_COLOR if ok else DROP_BAD_COLOR) + _set_cursor(("right" if outgoing else "left") if ok else "no") + app_state.set_status(reason, warn=not ok) + + +def _finish(event): + """Ends a press. Returns 'link' after a drag (made or refused), 'click' after a + press that never moved, None if no press on a slot was pending.""" + global _press, _drag + canvas = app_state.canvas + press, drag = _press, _drag + _press = _drag = None + canvas.delete("link_rubber") + canvas.delete("slot_hover") + _set_cursor(None) + + if drag is None: + return "click" if press is not None else None + + cx, cy = canvas.canvasx(event.x), canvas.canvasy(event.y) + hit = slot_at(cx, cy) + start = drag["hit"] + title_tag = title_at(cx, cy) if hit is None else None + if title_tag is not None: + other = app_state.canvas_comp_map.get(title_tag) + if other is None: + app_state.set_status("Link cancelled.") + elif start["side"] == "right": + _link_dialog(start["comp"], other, start["slot"], None) + else: + _link_dialog(other, start["comp"], None, start["slot"]) + return "link" + if hit is None or (hit["comp_tag"] == start["comp_tag"] and hit["slot"] == start["slot"]): + app_state.set_status("Link cancelled.") + return "link" + source, target = (start, hit) if start["side"] == "right" else (hit, start) + ok, reason = _check_link(source, target) + if not ok: + app_state.set_status(f"Cannot link: {reason}", warn=True) + return "link" + from_ref, to_ref = _refs(source, target) + sax_file.add_link(from_ref, to_ref) + app_state.set_status(f"Linked {from_ref} → {to_ref}") + return "link" + + +# Right button: on a slot it drags a link, and a press that never moves is the menu. + +def on_right_press(event): + if _start_press(event) is None: + _menu_callback(event) + + +def on_right_motion(event): + _drag_motion(event) + + +def on_right_release(event): + if _finish(event) == "click": + _menu_callback(event) + + +# Left button: anywhere on a box selects it. Only the title bar moves it; a slot row +# drags out a link, and the rest of the box does nothing more than select. + +def _on_title_bar(comp_tag, cy): + box = wiresheet.get_box_bounds(comp_tag) + return box is not None and box[1] <= cy <= box[1] + wiresheet.HEADER_HEIGHT * app_state.zoom + + +def on_left_press(event): + _box_handlers["press"](event) # selection, and arms a box move + if _start_press(event) is not None: + app_state.canvas.delete("slot_hover") + comp_tag = app_state.drag_data.get("tag") + if comp_tag and not _on_title_bar(comp_tag, app_state.canvas.canvasy(event.y)): + app_state.drag_data["tag"] = None # selected, but not picked up + + +def on_left_motion(event): + if _press is not None or _drag is not None: + _drag_motion(event) + else: + _box_handlers["motion"](event) + + +def on_left_release(event): + if _finish(event) is None: + _box_handlers["release"](event) + + +_box_handlers = {} + + +def install(canvas, menu_callback, box_press, box_motion, box_release, link_dialog): + """Binds hover, link dragging on both buttons, and the box handlers it wraps. + + menu_callback opens the context menu; box_* are the sheet's own left-button + select-and-move handlers, which still run whenever a press is not on a slot row; + link_dialog opens the Link dialog when a link is dropped on a title bar. + """ + global _menu_callback, _link_dialog + _menu_callback = menu_callback + _link_dialog = link_dialog + _box_handlers.update(press=box_press, motion=box_motion, release=box_release) + canvas.bind("", on_motion) + canvas.bind("", on_leave, add="+") + canvas.bind("", on_left_press) + canvas.bind("", on_left_motion) + canvas.bind("", on_left_release) + canvas.bind("", on_right_press) + canvas.bind("", on_right_motion) + canvas.bind("", on_right_release) diff --git a/main.py b/main.py index 799d25a..bb553a7 100644 --- a/main.py +++ b/main.py @@ -10,6 +10,7 @@ import threading import app_state import connect_dialog import device_session +import link_drag import link_view import live_hooks import manage_kits @@ -1201,8 +1202,12 @@ def _slot_linkable(slot_flags, slot_type, readonly, is_target): return True -def open_link_dialog(source_element, target_element): - """Opens a dialog to select source and target slots, validates types, and creates a link.""" +def open_link_dialog(source_element, target_element, source_slot=None, target_slot=None): + """Opens a dialog to select source and target slots, validates types, and creates a link. + + source_slot / target_slot pre-select a slot on that side, as when a link dragged out + of a slot is dropped on a component's title bar rather than on one of its slots. + """ window = app_state.window sides = [source_element, target_element] @@ -1346,6 +1351,11 @@ def open_link_dialog(source_element, target_element): ttk.Button(mid_frame, text="⇄", width=3, command=on_swap).pack(pady=5) refresh_panels() + for var, slot, info, is_target in ((src_var, source_slot, src_slot_info, False), + (tgt_var, target_slot, tgt_slot_info, True)): + if slot in info and _slot_linkable(info[slot][1], info[slot][0], info[slot][2], + is_target): + var.set(slot) status_label = tk.Label(main_frame, textvariable=status_var, anchor="w", fg="#333333") status_label.grid(row=1, column=0, columnspan=3, sticky="w", pady=(8, 0)) @@ -2804,7 +2814,11 @@ def refresh_palette_kits(): if _schema_has_incomplete_checksums(): palette_kit_names = [] else: - palette_kit_names = sorted(app_state.active_schema_kits.keys()) + # A kit with nothing placeable (all abstract or @palette=false) is left out. + palette_kit_names = [ + kit_name for kit_name in sorted(app_state.active_schema_kits.keys()) + if palette.list_component_types(kit_name) + ] labels = [] for kit_name in palette_kit_names: @@ -2966,12 +2980,13 @@ canvas.pack(fill=tk.BOTH, expand=True) canvas_vscroll.config(command=canvas.yview) canvas_hscroll.config(command=canvas.xview) -canvas.bind("", on_canvas_click) -canvas.bind("", on_canvas_drag) -canvas.bind("", on_canvas_release) canvas.bind("", lambda e: wiresheet.draw_grid()) -canvas.bind("", on_canvas_right_click) +# Either button pressed on a slot row drags out a new link. Elsewhere the left button +# selects and moves boxes and a plain right click is the menu, as before. +link_drag.install(canvas, on_canvas_right_click, + on_canvas_click, on_canvas_drag, on_canvas_release, + link_dialog=open_link_dialog) canvas.bind("", on_canvas_right_click) canvas.bind("", on_canvas_double_click) diff --git a/palette.py b/palette.py index 663eaea..3cac533 100644 --- a/palette.py +++ b/palette.py @@ -227,11 +227,25 @@ def _is_component(qualified_name, seen=None): return result +def _hidden_from_palette(type_element): + """True when the type's own facets carry @palette=false. + + Only the type-level counts; slots have of their own nested deeper. + """ + facets = type_element.find("facets") + if facets is None: + return False + for facet in facets.findall("bool"): + if facet.get("name") == "palette" and facet.get("val") == "false": + return True + return False + + def list_component_types(kit_name): """Concrete component types in a kit, as (type name, base) pairs. - Primitives (no base) and abstract types (the 'a' flag) are left out — neither can be - placed in an application. + Primitives (no base), abstract types (the 'a' flag) and types marked @palette=false + are left out — none of them belong in the palette. """ _, types = _load_kit(kit_name) @@ -239,6 +253,8 @@ def list_component_types(kit_name): for type_name, type_element in types.items(): if "a" in (type_element.get("flags") or ""): continue + if _hidden_from_palette(type_element): + continue if not _is_component(f"{kit_name}::{type_name}"): continue entries.append((type_name, type_element.get("base") or "")) diff --git a/wiresheet.py b/wiresheet.py index 75bb82b..b1b305d 100644 --- a/wiresheet.py +++ b/wiresheet.py @@ -948,6 +948,7 @@ def render_components(components_to_render): canvas.delete("component") canvas.delete("link") + canvas.delete("slot_hover") # link_drag's hover ring, stale once boxes repaint app_state.canvas_comp_map.clear() app_state.canvas_tag_by_element.clear() app_state.component_slot_offsets.clear()