git.lucas.co / cce-designer
graphic design tool
git clone https://git.lucas.co/cce-designer.git

commitfcb8e59edc9fdca09c7a5bfe764a7dbcf480b8cc
parent43cb7ce20c
authorLucas Galante <[email protected]>
date2026-06-02 13:25
Update system configuration and interface modules

 Cargo.lock                     |   62 ++
 Cargo.toml                     |    3 +
 scratch/find_click_offset.py   |   70 ++
 scratch/inspector_output.txt   |  415 +++++++++++
 scratch/test_circular_menu.py  |  110 +++
 scratch/test_flow.py           |   37 +
 scratch/test_focused_clicks.py |   71 ++
 scratch/test_offsets.py        |   71 ++
 src/main.rs                    | 1494 +++++++++++++++++++++++++++++-----------
 src/shader.wgsl                |   11 +
 10 files changed, 1935 insertions(+), 409 deletions(-)

diff --git a/Cargo.lock b/Cargo.lock
index f1c8216..2f3c713 100644
--- a/Cargo.lock
+++ b/Cargo.lock
@@ -187,6 +187,7 @@ dependencies = [
  "clear-ui",
  "glam",
  "glyphon",
+ "libc",
  "opencl3",
  "pollster",
  "raw-window-handle",
@@ -194,6 +195,7 @@ dependencies = [
  "serde_json",
  "smithay-client-toolkit",
  "tokio",
+ "toml",
  "wayland-client",
  "wgpu",
  "xkeysym",
@@ -207,6 +209,7 @@ dependencies = [
  "calloop",
  "calloop-wayland-source",
  "glyphon",
+ "libc",
  "pollster",
  "raw-window-handle",
  "resvg",
@@ -1338,6 +1341,15 @@ dependencies = [
  "zmij",
 ]
 
+[[package]]
+name = "serde_spanned"
+version = "0.6.9"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "bf41e0cfaf7226dca15e8197172c295a782857fcb97fad1808a166870dee75a3"
+dependencies = [
+ "serde",
+]
+
 [[package]]
 name = "shlex"
 version = "1.3.0"
@@ -1655,6 +1667,47 @@ dependencies = [
  "syn",
 ]
 
+[[package]]
+name = "toml"
+version = "0.8.23"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "dc1beb996b9d83529a9e75c17a1686767d148d70663143c7854d8b4a09ced362"
+dependencies = [
+ "serde",
+ "serde_spanned",
+ "toml_datetime",
+ "toml_edit",
+]
+
+[[package]]
+name = "toml_datetime"
+version = "0.6.11"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "22cddaf88f4fbc13c51aebbf5f8eceb5c7c5a9da2ac40a13519eb5b0a0e8f11c"
+dependencies = [
+ "serde",
+]
+
+[[package]]
+name = "toml_edit"
+version = "0.22.27"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "41fe8c660ae4257887cf66394862d21dbca4a6ddd26f04a3560410406a2f819a"
+dependencies = [
+ "indexmap",
+ "serde",
+ "serde_spanned",
+ "toml_datetime",
+ "toml_write",
+ "winnow",
+]
+
+[[package]]
+name = "toml_write"
+version = "0.1.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "5d99f8c9a7727884afe522e9bd5edbfc91a3312b36a77b5fb8926e4c31a41801"
+
 [[package]]
 name = "ttf-parser"
 version = "0.20.0"
@@ -2208,6 +2261,15 @@ version = "0.52.6"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec"
 
+[[package]]
+name = "winnow"
+version = "0.7.15"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "df79d97927682d2fd8adb29682d1140b343be4ac0f08fd68b7765d9c059d3945"
+dependencies = [
+ "memchr",
+]
+
 [[package]]
 name = "xcursor"
 version = "0.3.10"
diff --git a/Cargo.toml b/Cargo.toml
index 5d36f14..18e547a 100644
--- a/Cargo.toml
+++ b/Cargo.toml
@@ -19,5 +19,8 @@ tokio = { version = "1", features = ["full"] }
 glam = "0.29"
 serde = { version = "1", features = ["derive"] }
 serde_json = "1"
+toml = "0.8"
 opencl3 = "0.9"
+libc = "0.2"
+
 
diff --git a/scratch/find_click_offset.py b/scratch/find_click_offset.py
new file mode 100644
index 0000000..ebfe766
--- /dev/null
+++ b/scratch/find_click_offset.py
@@ -0,0 +1,70 @@
+import subprocess
+import time
+import re
+import sys
+import os
+
+def run_clearctl(cmd_str):
+    full_cmd = f"env WAYLAND_DISPLAY=wayland-0 XDG_RUNTIME_DIR=/run/user/1000 /home/lsgalante/.local/bin/clearctl {cmd_str}"
+    res = subprocess.run(full_cmd, shell=True, capture_output=True, text=True)
+    return res.stdout.strip()
+
+def get_window_rect(title):
+    stdout = run_clearctl("windows")
+    for line in stdout.splitlines():
+        if title in line:
+            m = re.search(r'\bx=(\d+)\s+y=(\d+)\s+w=(\d+)\s+h=(\d+)', line)
+            if m:
+                return int(m.group(1)), int(m.group(2)), int(m.group(3)), int(m.group(4))
+    return None
+
+log_path = "/home/lsgalante/Dropbox/Clear/design-interface.log"
+
+def clear_log():
+    if os.path.exists(log_path):
+        with open(log_path, "w") as f:
+            f.write("")
+
+def check_log_for_press():
+    if not os.path.exists(log_path):
+        return False
+    with open(log_path, "r", errors="ignore") as f:
+        content = f.read()
+    return "DEBUG" in content
+
+title = "Clear Design Interface"
+rect = get_window_rect(title)
+if not rect:
+    print(f"Error: Could not find window '{title}'", file=sys.stderr)
+    sys.exit(1)
+
+window_x, window_y, window_w, window_h = rect
+print(f"Window rect: x={window_x}, y={window_y}, w={window_w}, h={window_h}")
+
+# Focus the window first
+run_clearctl(f"focus-window \"{title}\"")
+time.sleep(0.5)
+
+# We will sweep dx from 50 to 200, dy from 0 to 100, in steps of 20
+found = False
+for dy in range(0, 150, 10):
+    for dx in range(50, 250, 20):
+        clear_log()
+        target_x = window_x + dx
+        target_y = window_y + dy
+        print(f"Testing client offset dx={dx}, dy={dy} -> screen ({target_x}, {target_y})")
+        
+        run_clearctl(f"pointer-move-to {target_x} {target_y}")
+        time.sleep(0.1)
+        run_clearctl("pointer-click left")
+        time.sleep(0.2)
+        
+        if check_log_for_press():
+            print(f"SUCCESS! Click registered at offset dx={dx}, dy={dy}")
+            found = True
+            break
+    if found:
+        break
+
+if not found:
+    print("Failed to register any clicks inside the window.")
diff --git a/scratch/inspector_output.txt b/scratch/inspector_output.txt
new file mode 100644
index 0000000..c05e679
--- /dev/null
+++ b/scratch/inspector_output.txt
@@ -0,0 +1,415 @@
+Found 5 active inspected surface(s):
+============================================================
+Surface #1:
+  Title:       foot
+  App ID:      foot
+  Position:    (x: 984, y: 48)
+  Dimensions:  912x540 px
+  Widget Tree: 
+    {}
+============================================================
+Surface #2:
+  Title:       clear-design-interface
+  App ID:      antigravity
+  Position:    (x: 24, y: 48)
+  Dimensions:  912x540 px
+  Widget Tree: 
+    {}
+============================================================
+Surface #3:
+  Title:       Clear Design Interface
+  App ID:      clear-design-interface
+  Position:    (x: 24, y: 636)
+  Dimensions:  912x540 px
+  Widget Tree: 
+    [
+      {
+        "children": [
+          {
+            "focused": false,
+            "hovered": false,
+            "label": "File",
+            "rect": [
+              197,
+              0,
+              46,
+              26
+            ],
+            "type": "Menu",
+            "value": 0
+          },
+          {
+            "focused": false,
+            "hovered": false,
+            "label": "Edit",
+            "rect": [
+              243,
+              0,
+              46,
+              26
+            ],
+            "type": "Menu",
+            "value": 0
+          },
+          {
+            "focused": false,
+            "hovered": false,
+            "label": "View",
+            "rect": [
+              289,
+              0,
+              46,
+              26
+            ],
+            "type": "Menu",
+            "value": 0
+          },
+          {
+            "focused": false,
+            "hovered": false,
+            "label": "Help",
+            "rect": [
+              335,
+              0,
+              46,
+              26
+            ],
+            "type": "Menu",
+            "value": 0
+          }
+        ],
+        "focused": false,
+        "hovered": false,
+        "label": "",
+        "rect": [
+          0,
+          0,
+          912,
+          26
+        ],
+        "type": "MenuBar",
+        "value": 0
+      },
+      {
+        "focused": false,
+        "hovered": false,
+        "label": "",
+        "rect": [
+          70,
+          215,
+          360,
+          265
+        ],
+        "type": "Graph",
+        "value": 0
+      },
+      {
+        "focused": false,
+        "hovered": false,
+        "label": "",
+        "rect": [
+          0,
+          0,
+          0,
+          0
+        ],
+        "type": "Splitter",
+        "value": 0
+      },
+      {
+        "focused": false,
+        "hovered": false,
+        "label": "",
+        "rect": [
+          0,
+          26,
+          606.57495,
+          486
+        ],
+        "type": "ViewportBg",
+        "value": 0
+      },
+      {
+        "focused": false,
+        "hovered": false,
+        "label": "",
+        "rect": [
+          606.57495,
+          26,
+          6,
+          486
+        ],
+        "type": "Splitter",
+        "value": 0
+      },
+      {
+        "focused": false,
+        "hovered": false,
+        "label": "",
+        "rect": [
+          612.57495,
+          26,
+          299.42505,
+          486
+        ],
+        "type": "ParametersBg",
+        "value": 0
+      },
+      {
+        "focused": false,
+        "hovered": false,
+        "label": "",
+        "rect": [
+          0,
+          26,
+          912,
+          486
+        ],
+        "type": "Canvas",
+        "value": 0
+      },
+      {
+        "children": [
+          {
+            "focused": false,
+            "hovered": false,
+            "label": "File",
+            "rect": [
+              226.50006,
+              120.00076,
+              46,
+              35
+            ],
+            "type": "Menu",
+            "value": 0
+          },
+          {
+            "focused": false,
+            "hovered": false,
+            "label": "Edit",
+            "rect": [
+              271.90784,
+              126.32849,
+              46,
+              35
+            ],
+            "type": "Menu",
+            "value": 0
+          },
+          {
+            "focused": false,
+            "hovered": false,
+            "label": "View",
+            "rect": [
+              313.74097,
+              145.0873,
+              46,
+              35
+            ],
+            "type": "Menu",
+            "value": 0
+          }
+        ],
+        "focused": false,
+        "hovered": false,
+        "label": "",
+        "rect": [
+          70,
+          120,
+          360,
+          35
+        ],
+        "type": "MenuBar",
+        "value": 0
+      },
+      {
+        "children": [
+          {
+            "focused": false,
+            "hovered": false,
+            "label": "Camera",
+            "rect": [
+              114.5,
+              26,
+              61,
+              26
+            ],
+            "type": "Menu",
+            "value": 0
+          },
+          {
+            "focused": false,
+            "hovered": false,
+            "label": "Display",
+            "rect": [
+              175.5,
+              26,
+              68.5,
+              26
+            ],
+            "type": "Menu",
+            "value": 0
+          },
+          {
+            "focused": false,
+            "hovered": false,
+            "label": "Guides",
+            "rect": [
+              244,
+              26,
+              61,
+              26
+            ],
+            "type": "Menu",
+            "value": 0
+          }
+        ],
+        "focused": false,
+        "hovered": false,
+        "label": "",
+        "rect": [
+          0,
+          26,
+          606.57495,
+          26
+        ],
+        "type": "MenuBar",
+        "value": 0
+      },
+      {
+        "children": [
+          {
+            "focused": false,
+            "hovered": false,
+            "label": "Preset",
+            "rect": [
+              742.07495,
+              26,
+              61,
+              26
+            ],
+            "type": "Menu",
+            "value": 0
+          },
+          {
+            "focused": false,
+            "hovered": false,
+            "label": "Reset",
+            "rect": [
+              803.07495,
+              26,
+              53.5,
+              26
+            ],
+            "type": "Menu",
+            "value": 0
+          }
+        ],
+        "focused": false,
+        "hovered": false,
+        "label": "",
+        "rect": [
+          612.57495,
+          26,
+          299.42505,
+          26
+        ],
+        "type": "MenuBar",
+        "value": 0
+      },
+      {
+        "focused": false,
+        "hovered": false,
+        "label": "",
+        "rect": [
+          0,
+          512,
+          912,
+          28
+        ],
+        "type": "StatusBar",
+        "value": 0
+      },
+      {
+        "focused": false,
+        "hovered": false,
+        "label": "",
+        "rect": [
+          70,
+          191,
+          360,
+          24
+        ],
+        "type": "Breadcrumb",
+        "value": 0
+      },
+      {
+        "focused": false,
+        "hovered": false,
+        "label": "",
+        "rect": [
+          0,
+          0,
+          912,
+          540
+        ],
+        "type": "ConfigDialog",
+        "value": 0
+      },
+      {
+        "focused": false,
+        "hovered": false,
+        "label": "",
+        "rect": [
+          0,
+          0,
+          912,
+          540
+        ],
+        "type": "NodePalette",
+        "value": 0
+      },
+      {
+        "focused": false,
+        "hovered": false,
+        "label": "",
+        "rect": [
+          0,
+          0,
+          0,
+          0
+        ],
+        "type": "Spreadsheet",
+        "value": 0
+      },
+      {
+        "focused": false,
+        "hovered": false,
+        "label": "",
+        "rect": [
+          0,
+          0,
+          0,
+          0
+        ],
+        "type": "MenuBar",
+        "value": 0
+      }
+    ]
+============================================================
+Surface #4:
+  Title:       when it is a circle the (Draft) - Text Editor
+  App ID:      org.gnome.TextEditor
+  Position:    (x: 24, y: 48)
+  Dimensions:  1872x1128 px
+  Widget Tree: 
+    {}
+============================================================
+Surface #5:
+  Title:       Clear Status Interface
+  App ID:      clear-status-interface
+  Position:    (x: 0, y: 0)
+  Dimensions:  1920x1200 px
+  Widget Tree: 
+    {}
+============================================================
diff --git a/scratch/test_circular_menu.py b/scratch/test_circular_menu.py
new file mode 100644
index 0000000..50a50e7
--- /dev/null
+++ b/scratch/test_circular_menu.py
@@ -0,0 +1,110 @@
+import subprocess
+import time
+import re
+import sys
+import os
+import math
+
+def run_clearctl(cmd_str):
+    full_cmd = f"env WAYLAND_DISPLAY=wayland-0 XDG_RUNTIME_DIR=/run/user/1000 /home/lsgalante/.local/bin/clearctl {cmd_str}"
+    res = subprocess.run(full_cmd, shell=True, capture_output=True, text=True)
+    return res.stdout.strip()
+
+def get_window_rect(title):
+    stdout = run_clearctl("windows")
+    for line in stdout.splitlines():
+        if title in line:
+            m = re.search(r'\bx=(\d+)\s+y=(\d+)\s+w=(\d+)\s+h=(\d+)', line)
+            if m:
+                return int(m.group(1)), int(m.group(2)), int(m.group(3)), int(m.group(4))
+    return None
+
+def reset_pointer(window_x, window_y, window_w, window_h):
+    cx = window_x + window_w // 2
+    cy = window_y + window_h // 2
+    print(f"Focusing window at ({cx}, {cy})...")
+    run_clearctl(f"pointer-move-to {cx} {cy}")
+    time.sleep(0.2)
+    run_clearctl("pointer-click left")
+    time.sleep(0.3)
+    run_clearctl("key-press Escape")
+    time.sleep(0.05)
+    run_clearctl("key-release Escape")
+    time.sleep(0.2)
+
+def get_curved_circle_params():
+    debug_path = "/home/lsgalante/Dropbox/Clear/debug.txt"
+    if not os.path.exists(debug_path):
+        return None
+    with open(debug_path, "r") as f:
+        lines = f.readlines()
+    for line in reversed(lines):
+        # Match curved=Some((298.9297, 279.10547, 180.0))
+        m = re.search(r'curved=Some\(\(([\d\.-]+),\s*([\d\.-]+),\s*([\d\.-]+)\)\)', line)
+        if m:
+            return float(m.group(1)), float(m.group(2)), float(m.group(3))
+    return None
+
+title = "Clear Design Interface"
+rect = get_window_rect(title)
+if not rect:
+    print(f"Error: Could not find window '{title}'", file=sys.stderr)
+    sys.exit(1)
+
+window_x, window_y, window_w, window_h = rect
+print(f"Found window '{title}' at x={window_x}, y={window_y}, w={window_w}, h={window_h}")
+
+reset_pointer(window_x, window_y, window_w, window_h)
+
+circle_params = get_curved_circle_params()
+if not circle_params:
+    print("Error: Could not read curved circle parameters from debug.txt. Using defaults.", file=sys.stderr)
+    circle_params = (298.9297, 279.10547, 180.0)
+
+cx, cy, r = circle_params
+print(f"Curved circle center from debug.txt: cx={cx}, cy={cy}, r={r}")
+
+# View menu midpoint angle:
+theta = (5.1339273 + 5.417004) / 2.0
+r_mid = r - 17.5
+
+local_x = cx + r_mid * math.cos(theta)
+local_y = cy + r_mid * math.sin(theta)
+
+border_offset_x = 0
+border_offset_y = 0
+
+scale = 2.0
+screen_x = int(window_x + border_offset_x + local_x * scale)
+screen_y = int(window_y + border_offset_y + local_y * scale)
+
+print(f"Targeting client local ({local_x:.2f}, {local_y:.2f}) -> screen ({screen_x}, {screen_y})")
+
+log_path = "/home/lsgalante/Dropbox/Clear/design-interface.log"
+initial_size = 0
+if os.path.exists(log_path):
+    initial_size = os.path.getsize(log_path)
+
+print(f"Moving pointer to ({screen_x}, {screen_y})...")
+print("Move result:", run_clearctl(f"pointer-move-to {screen_x} {screen_y}"))
+time.sleep(0.5)
+
+print("Clicking left...")
+print("Click result:", run_clearctl("pointer-click left"))
+time.sleep(1.0)
+
+# Check design-interface.log for new DEBUG output
+new_logs = ""
+if os.path.exists(log_path):
+    with open(log_path, "r", errors="ignore") as f:
+        f.seek(initial_size)
+        new_logs = f.read()
+
+print("--- New Application Logs ---")
+print(new_logs)
+print("----------------------------")
+
+screenshot_path = "/home/lsgalante/.gemini/antigravity/brain/9048aec8-21a4-458f-94d4-efe127095c7f/screenshot_circular_view_dropdown.png"
+print(f"Taking screenshot: {screenshot_path}")
+subprocess.run(f"env WAYLAND_DISPLAY=wayland-0 XDG_RUNTIME_DIR=/run/user/1000 grim {screenshot_path}", shell=True)
+print("Done.")
diff --git a/scratch/test_flow.py b/scratch/test_flow.py
new file mode 100644
index 0000000..6f630af
--- /dev/null
+++ b/scratch/test_flow.py
@@ -0,0 +1,37 @@
+import subprocess
+import time
+
+def run_clearctl(cmd_str):
+    full_cmd = f"env WAYLAND_DISPLAY=wayland-0 XDG_RUNTIME_DIR=/run/user/1000 /home/lsgalante/.local/bin/clearctl {cmd_str}"
+    res = subprocess.run(full_cmd, shell=True, capture_output=True, text=True)
+    return res.stdout.strip()
+
+def click_at(x, y):
+    print(f"Moving to {x}, {y}...")
+    run_clearctl(f"pointer-move-to {x} {y}")
+    time.sleep(0.2)
+    print(f"Clicking at {x}, {y}...")
+    run_clearctl("pointer-click left")
+    time.sleep(0.3)
+
+# Reset cursor & close any menus
+run_clearctl("key-press Escape")
+time.sleep(0.05)
+run_clearctl("key-release Escape")
+time.sleep(0.2)
+
+# Focus the window
+click_at(1100, 150)
+
+# Move to View menu and click
+# View menu is around local x=230 (global 984+230 = 1214), local y=39 (global 48+39 = 87)
+click_at(1214, 87)
+
+# Move to "Circular Pane" dropdown and click
+# Dropdown is at x=207 to 344. Let's use local x=270 (global 984+270 = 1254).
+# Y center is local y=107 (global 48+107 = 155).
+click_at(1254, 155)
+
+print("Taking screenshot...")
+subprocess.run("env WAYLAND_DISPLAY=wayland-0 XDG_RUNTIME_DIR=/run/user/1000 grim /home/lsgalante/.gemini/antigravity/brain/9048aec8-21a4-458f-94d4-efe127095c7f/screenshot_test.png", shell=True)
+print("Done.")
diff --git a/scratch/test_focused_clicks.py b/scratch/test_focused_clicks.py
new file mode 100644
index 0000000..843d08d
--- /dev/null
+++ b/scratch/test_focused_clicks.py
@@ -0,0 +1,71 @@
+import subprocess
+import time
+import re
+import sys
+
+def run_clearctl(cmd_str):
+    full_cmd = f"env WAYLAND_DISPLAY=wayland-0 XDG_RUNTIME_DIR=/run/user/1000 /home/lsgalante/.local/bin/clearctl {cmd_str}"
+    res = subprocess.run(full_cmd, shell=True, capture_output=True, text=True)
+    return res.stdout.strip()
+
+def get_window_rect(title):
+    stdout = run_clearctl("windows")
+    for line in stdout.splitlines():
+        if title in line:
+            m = re.search(r'\bx=(\d+)\s+y=(\d+)\s+w=(\d+)\s+h=(\d+)', line)
+            if m:
+                return int(m.group(1)), int(m.group(2)), int(m.group(3)), int(m.group(4))
+    return None
+
+def reset_pointer():
+    run_clearctl("key-press Escape")
+    time.sleep(0.05)
+    run_clearctl("key-release Escape")
+    time.sleep(0.2)
+
+title = "Clear Design Interface"
+rect = get_window_rect(title)
+if not rect:
+    print(f"Error: Could not find window '{title}'", file=sys.stderr)
+    sys.exit(1)
+
+window_x, window_y, window_w, window_h = rect
+print(f"Window: x={window_x}, y={window_y}, w={window_w}, h={window_h}")
+
+# View menu local coordinates (under circular mode):
+# center x = 313.7 + 23.0 = 336.7
+# center y = 145.1 + 17.5 = 162.6
+view_local_x = 336.7
+view_local_y = 162.6
+
+# Test offset = 0
+print("--- Testing Offset 0 ---")
+reset_pointer()
+print(run_clearctl(f"focus-window \"{title}\""))
+time.sleep(0.3)
+x0 = int(window_x + view_local_x)
+y0 = int(window_y + view_local_y)
+print(f"Moving to ({x0}, {y0})")
+run_clearctl(f"pointer-move-to {x0} {y0}")
+time.sleep(0.3)
+print("Clicking left...")
+run_clearctl("pointer-click left")
+time.sleep(1.0)
+subprocess.run("env WAYLAND_DISPLAY=wayland-0 XDG_RUNTIME_DIR=/run/user/1000 grim /home/lsgalante/.gemini/antigravity/brain/9048aec8-21a4-458f-94d4-efe127095c7f/screenshot_view_offset_0.png", shell=True)
+
+# Test offset = 24
+print("--- Testing Offset 24 ---")
+reset_pointer()
+print(run_clearctl(f"focus-window \"{title}\""))
+time.sleep(0.3)
+x24 = int(window_x + 24 + view_local_x)
+y24 = int(window_y + 24 + view_local_y)
+print(f"Moving to ({x24}, {y24})")
+run_clearctl(f"pointer-move-to {x24} {y24}")
+time.sleep(0.3)
+print("Clicking left...")
+run_clearctl("pointer-click left")
+time.sleep(1.0)
+subprocess.run("env WAYLAND_DISPLAY=wayland-0 XDG_RUNTIME_DIR=/run/user/1000 grim /home/lsgalante/.gemini/antigravity/brain/9048aec8-21a4-458f-94d4-efe127095c7f/screenshot_view_offset_24.png", shell=True)
+
+print("Done.")
diff --git a/scratch/test_offsets.py b/scratch/test_offsets.py
new file mode 100644
index 0000000..29e248d
--- /dev/null
+++ b/scratch/test_offsets.py
@@ -0,0 +1,71 @@
+import subprocess
+import time
+import re
+import sys
+import os
+
+def run_clearctl(cmd_str):
+    full_cmd = f"env WAYLAND_DISPLAY=wayland-0 XDG_RUNTIME_DIR=/run/user/1000 /home/lsgalante/.local/bin/clearctl {cmd_str}"
+    res = subprocess.run(full_cmd, shell=True, capture_output=True, text=True)
+    return res.stdout.strip()
+
+def get_window_rect(title):
+    stdout = run_clearctl("windows")
+    for line in stdout.splitlines():
+        if title in line:
+            m = re.search(r'\bx=(\d+)\s+y=(\d+)\s+w=(\d+)\s+h=(\d+)', line)
+            if m:
+                return int(m.group(1)), int(m.group(2)), int(m.group(3)), int(m.group(4))
+    return None
+
+def reset_pointer(window_x, window_y, window_w, window_h):
+    # Click at window center to focus it
+    cx = window_x + window_w // 2
+    cy = window_y + window_h // 2
+    print(f"Focusing window at ({cx}, {cy})...")
+    run_clearctl(f"pointer-move-to {cx} {cy}")
+    time.sleep(0.2)
+    run_clearctl("pointer-click left")
+    time.sleep(0.3)
+    # Press Escape to dismiss any open menus
+    run_clearctl("key-press Escape")
+    time.sleep(0.05)
+    run_clearctl("key-release Escape")
+    time.sleep(0.2)
+
+title = "Clear Design Interface"
+rect = get_window_rect(title)
+if not rect:
+    print(f"Error: Could not find window '{title}'", file=sys.stderr)
+    sys.exit(1)
+
+window_x, window_y, window_w, window_h = rect
+print(f"Window: x={window_x}, y={window_y}, w={window_w}, h={window_h}")
+
+# File menu local coordinates
+file_local_x = 250.5
+file_local_y = 137.5
+
+# Test offset = 0
+reset_pointer(window_x, window_y, window_w, window_h)
+x0 = int(window_x + file_local_x)
+y0 = int(window_y + file_local_y)
+print(f"Testing Offset 0: screen ({x0}, {y0})")
+run_clearctl(f"pointer-move-to {x0} {y0}")
+time.sleep(0.3)
+run_clearctl("pointer-click left")
+time.sleep(1.0)
+subprocess.run("env WAYLAND_DISPLAY=wayland-0 XDG_RUNTIME_DIR=/run/user/1000 grim /home/lsgalante/.gemini/antigravity/brain/9048aec8-21a4-458f-94d4-efe127095c7f/screenshot_offset_0.png", shell=True)
+
+# Test offset = 24
+reset_pointer(window_x, window_y, window_w, window_h)
+x24 = int(window_x + 24 + file_local_x)
+y24 = int(window_y + 24 + file_local_y)
+print(f"Testing Offset 24: screen ({x24}, {y24})")
+run_clearctl(f"pointer-move-to {x24} {y24}")
+time.sleep(0.3)
+run_clearctl("pointer-click left")
+time.sleep(1.0)
+subprocess.run("env WAYLAND_DISPLAY=wayland-0 XDG_RUNTIME_DIR=/run/user/1000 grim /home/lsgalante/.gemini/antigravity/brain/9048aec8-21a4-458f-94d4-efe127095c7f/screenshot_offset_24.png", shell=True)
+
+print("Done testing offsets.")
diff --git a/src/main.rs b/src/main.rs
index 7e472c8..81008ef 100644
--- a/src/main.rs
+++ b/src/main.rs
@@ -79,7 +79,7 @@ enum WindowEvent {
 
 use wgpu::util::DeviceExt;
 
-use clear_ui::widget::{Breadcrumb, Canvas, ColorSelector, ContentBg, MenuBar, Node, ParametersBg, Spinbox, Splitter, Spreadsheet, StatusBar, TextLabel, Toggle, ViewportBg, Widget};
+use clear_ui::widget::{Breadcrumb, Canvas, ColorSelector, ContentBg, MenuBar, Node, ParametersBg, Spinbox, Splitter, Spreadsheet, StatusBar, TextLabel, Toggle, ViewportBg, Widget, GraphNode, Graph};
 use clear_ui::colors;
 use clear_ui::layout::{RenderTarget, Section};
 
@@ -100,16 +100,15 @@ const CANVAS_IDX: usize = 6;
 const LEFT_MENUBAR_IDX: usize = 7;
 const RIGHT_MENUBAR_IDX: usize = 8;
 const PARAM_MENUBAR_IDX: usize = 9;
-const NODE_SLOT_START: usize = 10;
-const NODE_SLOT_COUNT: usize = 32;
-const STATUS_IDX: usize = NODE_SLOT_START + NODE_SLOT_COUNT;
-const BREADCRUMB_IDX: usize = STATUS_IDX + 1;
-const CONFIG_DIALOG_IDX: usize = BREADCRUMB_IDX + 1;
-const NODE_PALETTE_IDX: usize = CONFIG_DIALOG_IDX + 1;
-const SPREADSHEET_IDX: usize = NODE_PALETTE_IDX + 1;
-const SPREADSHEET_MENUBAR_IDX: usize = SPREADSHEET_IDX + 1;
-
-const HEADER_H: f32 = 40.0;
+const STATUS_IDX: usize = 10;
+const BREADCRUMB_IDX: usize = 11;
+const CONFIG_DIALOG_IDX: usize = 12;
+const NODE_PALETTE_IDX: usize = 13;
+const SPREADSHEET_IDX: usize = 14;
+const SPREADSHEET_MENUBAR_IDX: usize = 15;
+
+
+const HEADER_H: f32 = 26.0;
 const STATUS_H: f32 = 28.0;
 const MENUBAR_H: f32 = 26.0;
 const SPLITTER_W: f32 = 6.0;
@@ -196,6 +195,7 @@ enum HttpAction {
     MoveNode { slot: usize, x: f32, y: f32 },
     AddParam { slot: usize, name: String, param_type: String, default: String },
     DeleteParam { slot: usize, name: String },
+    ToggleCircularPane,
 }
 
 #[derive(Debug)]
@@ -282,6 +282,7 @@ fn load_fs_tree() -> FsNode {
 fn default_grid_thickness() -> f32 { 0.03 }
 fn default_show_camera_pivot() -> bool { false }
 fn default_camera_pivot_size() -> f32 { 1.0 }
+fn default_node_color() -> [f32; 3] { [0.10, 0.45, 0.70] }
 
 #[derive(Serialize, Deserialize, Clone, Debug)]
 struct DesignSettings {
@@ -303,6 +304,8 @@ struct DesignSettings {
     show_camera_pivot_enabled: bool,
     #[serde(default = "default_camera_pivot_size")]
     camera_pivot_size: f32,
+    #[serde(default = "default_node_color")]
+    node_color: [f32; 3],
 }
 
 impl Default for DesignSettings {
@@ -323,6 +326,7 @@ impl Default for DesignSettings {
             grid_thickness: 0.03,
             show_camera_pivot_enabled: false,
             camera_pivot_size: 1.0,
+            node_color: default_node_color(),
         }
     }
 }
@@ -435,6 +439,8 @@ struct ConfigDialog {
     viewport_b: f32,
     color_selector: ColorSelector,
     last_sent_color: [u8; 3],
+    node_color_selector: ColorSelector,
+    last_sent_node_color: [u8; 3],
     origin_size: f32,
     camera_pivot_size: f32,
     grid_thickness: f32,
@@ -459,6 +465,9 @@ impl ConfigDialog {
         let r = (vr * 255.0).round().clamp(0.0, 255.0) as u8;
         let g = (vg * 255.0).round().clamp(0.0, 255.0) as u8;
         let b = (vb * 255.0).round().clamp(0.0, 255.0) as u8;
+        let nr = (settings.node_color[0] * 255.0).round().clamp(0.0, 255.0) as u8;
+        let ng = (settings.node_color[1] * 255.0).round().clamp(0.0, 255.0) as u8;
+        let nb = (settings.node_color[2] * 255.0).round().clamp(0.0, 255.0) as u8;
 
         let mut toggle_grid_snap = Toggle::new().with_label("Snap to Grid");
         toggle_grid_snap.set_toggled(settings.grid_snap_enabled);
@@ -495,6 +504,8 @@ impl ConfigDialog {
             viewport_b: vb,
             color_selector: ColorSelector::new([r, g, b]).with_label("Viewport Background"),
             last_sent_color: [r, g, b],
+            node_color_selector: ColorSelector::new([nr, ng, nb]).with_label("Node Base Color"),
+            last_sent_node_color: [nr, ng, nb],
             origin_size: settings.origin_size,
             camera_pivot_size: settings.camera_pivot_size,
             grid_thickness: settings.grid_thickness,
@@ -661,7 +672,16 @@ impl ConfigDialog {
             collector.active = true;
             let y2 = sec2.finish(&mut collector);
 
-            let total_h = y2 - (py + 65.0);
+            // Section 3: Node Styling Section (aligned vertically under sec2)
+            collector.active = true;
+            let mut sec3 = Section::new(&mut collector, px + 20.0, y2 + 12.0, 760.0, "Node Styling");
+            collector.active = false;
+            sec3.widget(&mut collector, &mut self.node_color_selector, 14.0, 240.0, 24.0);
+            sec3.spacing(8.0);
+            collector.active = true;
+            let y3 = sec3.finish(&mut collector);
+
+            let total_h = y3 - (py + 65.0);
             let visible_h = 510.0;
             max_scroll_y = (total_h - visible_h).max(0.0);
         }
@@ -729,6 +749,7 @@ impl ConfigDialog {
                 shift_widget(&mut self.spin_grid_y);
                 shift_widget(&mut self.spin_skipped_row_h);
                 shift_widget(&mut self.spin_skipped_col_w);
+                shift_widget(&mut self.node_color_selector);
             } else if self.active_page == 2 {
                 shift_widget(&mut self.toggle_show_grid);
                 shift_widget(&mut self.toggle_show_cube);
@@ -870,6 +891,18 @@ impl Widget for ConfigDialog {
                 self.camera_pivot_size = val;
                 self.spin_camera_pivot_size.value = (val * 10.0).round() as i32;
             }
+            10 => {
+                self.node_color_selector.color[0] = (val * 255.0).round().clamp(0.0, 255.0) as u8;
+                self.last_sent_node_color[0] = self.node_color_selector.color[0];
+            }
+            11 => {
+                self.node_color_selector.color[1] = (val * 255.0).round().clamp(0.0, 255.0) as u8;
+                self.last_sent_node_color[1] = self.node_color_selector.color[1];
+            }
+            12 => {
+                self.node_color_selector.color[2] = (val * 255.0).round().clamp(0.0, 255.0) as u8;
+                self.last_sent_node_color[2] = self.node_color_selector.color[2];
+            }
             _ => {}
         }
     }
@@ -915,6 +948,13 @@ impl Widget for ConfigDialog {
                 return Some((4 + i, val));
             }
         }
+        for i in 0..3 {
+            if self.node_color_selector.color[i] != self.last_sent_node_color[i] {
+                let val = self.node_color_selector.color[i] as f32 / 255.0;
+                self.last_sent_node_color[i] = self.node_color_selector.color[i];
+                return Some((10 + i, val));
+            }
+        }
         None
     }
 
@@ -950,6 +990,16 @@ impl Widget for ConfigDialog {
             was
         };
 
+        let cs_node_changed = if self.visible && self.active_page == 1 {
+            self.node_color_selector.cursor_moved(px, py)
+        } else {
+            let was = self.node_color_selector.hovered();
+            self.node_color_selector.set_hovered(false);
+            was
+        };
+
+        let cs_changed = cs_changed || cs_node_changed;
+
         let mut widgets_changed = false;
         if self.visible {
             if self.active_page == 1 {
@@ -1014,6 +1064,7 @@ impl Widget for ConfigDialog {
         if state == ElementState::Pressed {
             if !in_panel || on_close {
                 self.color_selector.unfocus();
+                self.node_color_selector.unfocus();
                 self.spin_grid_x.unfocus();
                 self.spin_grid_y.unfocus();
                 self.spin_skipped_row_h.unfocus();
@@ -1033,6 +1084,7 @@ impl Widget for ConfigDialog {
             for (i, (tx, ty, tw, th)) in self.tab_rects(ppx, ppy).iter().enumerate() {
                 if px >= *tx && px < *tx + *tw && py >= *ty && py < *ty + *th {
                     self.color_selector.unfocus();
+                    self.node_color_selector.unfocus();
                     self.spin_grid_x.unfocus();
                     self.spin_grid_y.unfocus();
                     self.spin_skipped_row_h.unfocus();
@@ -1064,6 +1116,7 @@ impl Widget for ConfigDialog {
                 self.spin_grid_y.unfocus();
                 self.spin_skipped_row_h.unfocus();
                 self.spin_skipped_col_w.unfocus();
+                self.node_color_selector.unfocus();
                 handled = true;
             } else if self.toggle_network_grid.mouse_input(button, state, px, py) {
                 self.toggle_grid_snap.unfocus();
@@ -1071,6 +1124,7 @@ impl Widget for ConfigDialog {
                 self.spin_grid_y.unfocus();
                 self.spin_skipped_row_h.unfocus();
                 self.spin_skipped_col_w.unfocus();
+                self.node_color_selector.unfocus();
                 handled = true;
             } else if self.spin_grid_x.mouse_input(button, state, px, py) {
                 self.toggle_grid_snap.unfocus();
@@ -1078,6 +1132,7 @@ impl Widget for ConfigDialog {
                 self.spin_grid_y.unfocus();
                 self.spin_skipped_row_h.unfocus();
                 self.spin_skipped_col_w.unfocus();
+                self.node_color_selector.unfocus();
                 handled = true;
             } else if self.spin_grid_y.mouse_input(button, state, px, py) {
                 self.toggle_grid_snap.unfocus();
@@ -1085,6 +1140,7 @@ impl Widget for ConfigDialog {
                 self.spin_grid_x.unfocus();
                 self.spin_skipped_row_h.unfocus();
                 self.spin_skipped_col_w.unfocus();
+                self.node_color_selector.unfocus();
                 handled = true;
             } else if self.spin_skipped_row_h.mouse_input(button, state, px, py) {
                 self.toggle_grid_snap.unfocus();
@@ -1092,6 +1148,7 @@ impl Widget for ConfigDialog {
                 self.spin_grid_x.unfocus();
                 self.spin_grid_y.unfocus();
                 self.spin_skipped_col_w.unfocus();
+                self.node_color_selector.unfocus();
                 handled = true;
             } else if self.spin_skipped_col_w.mouse_input(button, state, px, py) {
                 self.toggle_grid_snap.unfocus();
@@ -1099,6 +1156,15 @@ impl Widget for ConfigDialog {
                 self.spin_grid_x.unfocus();
                 self.spin_grid_y.unfocus();
                 self.spin_skipped_row_h.unfocus();
+                self.node_color_selector.unfocus();
+                handled = true;
+            } else if self.node_color_selector.mouse_input(button, state, px, py) {
+                self.toggle_grid_snap.unfocus();
+                self.toggle_network_grid.unfocus();
+                self.spin_grid_x.unfocus();
+                self.spin_grid_y.unfocus();
+                self.spin_skipped_row_h.unfocus();
+                self.spin_skipped_col_w.unfocus();
                 handled = true;
             }
         } else if self.active_page == 2 {
@@ -1110,6 +1176,7 @@ impl Widget for ConfigDialog {
                 self.spin_origin_size.unfocus();
                 self.spin_camera_pivot_size.unfocus();
                 self.color_selector.unfocus();
+                self.node_color_selector.unfocus();
                 handled = true;
             } else if self.toggle_show_cube.mouse_input(button, state, px, py) {
                 self.toggle_show_grid.unfocus();
@@ -1119,6 +1186,7 @@ impl Widget for ConfigDialog {
                 self.spin_origin_size.unfocus();
                 self.spin_camera_pivot_size.unfocus();
                 self.color_selector.unfocus();
+                self.node_color_selector.unfocus();
                 handled = true;
             } else if self.toggle_show_origin.mouse_input(button, state, px, py) {
                 self.toggle_show_grid.unfocus();
@@ -1128,6 +1196,7 @@ impl Widget for ConfigDialog {
                 self.spin_origin_size.unfocus();
                 self.spin_camera_pivot_size.unfocus();
                 self.color_selector.unfocus();
+                self.node_color_selector.unfocus();
                 handled = true;
             } else if self.toggle_show_camera_pivot.mouse_input(button, state, px, py) {
                 self.toggle_show_grid.unfocus();
@@ -1137,6 +1206,7 @@ impl Widget for ConfigDialog {
                 self.spin_origin_size.unfocus();
                 self.spin_camera_pivot_size.unfocus();
                 self.color_selector.unfocus();
+                self.node_color_selector.unfocus();
                 handled = true;
             } else if self.spin_grid_thickness.mouse_input(button, state, px, py) {
                 self.toggle_show_grid.unfocus();
@@ -1146,6 +1216,7 @@ impl Widget for ConfigDialog {
                 self.spin_origin_size.unfocus();
                 self.spin_camera_pivot_size.unfocus();
                 self.color_selector.unfocus();
+                self.node_color_selector.unfocus();
                 handled = true;
             } else if self.spin_origin_size.mouse_input(button, state, px, py) {
                 self.toggle_show_grid.unfocus();
@@ -1155,6 +1226,7 @@ impl Widget for ConfigDialog {
                 self.spin_grid_thickness.unfocus();
                 self.spin_camera_pivot_size.unfocus();
                 self.color_selector.unfocus();
+                self.node_color_selector.unfocus();
                 handled = true;
             } else if self.spin_camera_pivot_size.mouse_input(button, state, px, py) {
                 self.toggle_show_grid.unfocus();
@@ -1164,6 +1236,7 @@ impl Widget for ConfigDialog {
                 self.spin_grid_thickness.unfocus();
                 self.spin_origin_size.unfocus();
                 self.color_selector.unfocus();
+                self.node_color_selector.unfocus();
                 handled = true;
             } else if self.color_selector.mouse_input(button, state, px, py) {
                 self.toggle_show_grid.unfocus();
@@ -1173,6 +1246,7 @@ impl Widget for ConfigDialog {
                 self.spin_grid_thickness.unfocus();
                 self.spin_origin_size.unfocus();
                 self.spin_camera_pivot_size.unfocus();
+                self.node_color_selector.unfocus();
                 handled = true;
             }
         }
@@ -1191,6 +1265,7 @@ impl Widget for ConfigDialog {
             self.spin_origin_size.unfocus();
             self.spin_camera_pivot_size.unfocus();
             self.color_selector.unfocus();
+            self.node_color_selector.unfocus();
             self.toggle_grid_snap.unfocus();
             self.toggle_network_grid.unfocus();
             self.toggle_show_grid.unfocus();
@@ -1209,6 +1284,7 @@ impl Widget for ConfigDialog {
             if self.spin_grid_y.keyboard_input(event) { return true; }
             if self.spin_skipped_row_h.keyboard_input(event) { return true; }
             if self.spin_skipped_col_w.keyboard_input(event) { return true; }
+            if self.node_color_selector.keyboard_input(event) { return true; }
         } else if self.active_page == 2 {
             if self.spin_grid_thickness.keyboard_input(event) { return true; }
             if self.spin_origin_size.keyboard_input(event) { return true; }
@@ -1217,6 +1293,7 @@ impl Widget for ConfigDialog {
         }
         if event.state == ElementState::Pressed && event.logical_key == Key::Named(NamedKey::Escape) {
             self.color_selector.unfocus();
+            self.node_color_selector.unfocus();
             self.spin_grid_x.unfocus();
             self.spin_grid_y.unfocus();
             self.spin_skipped_row_h.unfocus();
@@ -1280,6 +1357,7 @@ impl Widget for ConfigDialog {
         if self.active_page == 1 {
             quads.extend(filter_quads(self.toggle_grid_snap.extra_quads()));
             quads.extend(filter_quads(self.toggle_network_grid.extra_quads()));
+            quads.extend(filter_quads(self.node_color_selector.extra_quads()));
 
             let draw_spin = |spin: &Spinbox, q: &mut Vec<(f32, f32, f32, f32, [f32; 4])>| {
                 let (sx, sy, sw, sh) = spin.rect();
@@ -1359,6 +1437,7 @@ impl Widget for ConfigDialog {
         if self.active_page == 1 {
             labels.extend(filter_labels(self.toggle_grid_snap.text_labels()));
             labels.extend(filter_labels(self.toggle_network_grid.text_labels()));
+            labels.extend(filter_labels(self.node_color_selector.text_labels()));
 
             labels.extend(filter_labels(self.spin_grid_x.text_labels()));
             labels.extend(filter_labels(self.spin_grid_y.text_labels()));
@@ -1487,6 +1566,7 @@ enum Action {
     ToggleSpreadsheet,
     ToggleOrigin,
     ToggleCameraPivot,
+    ToggleCircularPane,
 }
 
 struct KeyBind {
@@ -1515,6 +1595,7 @@ fn default_keybinds() -> Vec<KeyBind> {
         KeyBind { ctrl: true, shift: false, alt: false, super_: false, key: Key::Character("a".into()), action: Action::ToggleSquareViewport },
         KeyBind { ctrl: true, shift: false, alt: false, super_: false, key: Key::Character(",".into()), action: Action::ToggleConfigure },
         KeyBind { ctrl: false, shift: false, alt: false, super_: false, key: Key::Character("`".into()), action: Action::ToggleSpreadsheet },
+        KeyBind { ctrl: true, shift: false, alt: false, super_: false, key: Key::Character("d".into()), action: Action::ToggleCircularPane },
     ]
 }
 
@@ -1523,10 +1604,15 @@ fn default_keybinds() -> Vec<KeyBind> {
 struct Vertex {
     position: [f32; 2],
     color: [f32; 4],
+    clip_circle: [f32; 3], // [cx, cy, r]
 }
 
 impl Vertex {
-    const ATTRIBS: [wgpu::VertexAttribute; 2] = wgpu::vertex_attr_array![0 => Float32x2, 1 => Float32x4];
+    const ATTRIBS: [wgpu::VertexAttribute; 3] = wgpu::vertex_attr_array![
+        0 => Float32x2,
+        1 => Float32x4,
+        2 => Float32x3,
+    ];
 
     fn desc() -> wgpu::VertexBufferLayout<'static> {
         wgpu::VertexBufferLayout {
@@ -1542,24 +1628,25 @@ fn quad_vertices(
     x: f32, y: f32, w: f32, h: f32,
     surface_w: f32, surface_h: f32,
     color: [f32; 4],
+    clip_circle: [f32; 3],
 ) -> [Vertex; 6] {
     let x0 = (x / surface_w) * 2.0 - 1.0;
     let y0 = 1.0 - (y / surface_h) * 2.0;
     let x1 = ((x + w) / surface_w) * 2.0 - 1.0;
     let y1 = 1.0 - ((y + h) / surface_h) * 2.0;
     [
-        Vertex { position: [x0, y0], color },
-        Vertex { position: [x1, y0], color },
-        Vertex { position: [x0, y1], color },
-        Vertex { position: [x1, y0], color },
-        Vertex { position: [x1, y1], color },
-        Vertex { position: [x0, y1], color },
+        Vertex { position: [x0, y0], color, clip_circle },
+        Vertex { position: [x1, y0], color, clip_circle },
+        Vertex { position: [x0, y1], color, clip_circle },
+        Vertex { position: [x1, y0], color, clip_circle },
+        Vertex { position: [x1, y1], color, clip_circle },
+        Vertex { position: [x0, y1], color, clip_circle },
     ]
 }
 
-fn widget_vertices(w: &dyn Widget, sw: f32, sh: f32) -> Vec<Vertex> {
+fn widget_vertices(w: &dyn Widget, sw: f32, sh: f32, clip_circle: [f32; 3]) -> Vec<Vertex> {
     let (x, y, ww, h) = w.rect();
-    quad_vertices(x, y, ww, h, sw, sh, w.color()).to_vec()
+    quad_vertices(x, y, ww, h, sw, sh, w.color(), clip_circle).to_vec()
 }
 
 fn quad_vertices_clipped(
@@ -1567,6 +1654,7 @@ fn quad_vertices_clipped(
     surface_w: f32, surface_h: f32,
     color: [f32; 4],
     clip: (f32, f32, f32, f32),
+    clip_circle: [f32; 3],
 ) -> Vec<Vertex> {
     let (cx0, cy0, cx1, cy1) = clip;
     let ix0 = x.max(cx0);
@@ -1576,12 +1664,123 @@ fn quad_vertices_clipped(
     if ix1 <= ix0 || iy1 <= iy0 {
         return Vec::new();
     }
-    quad_vertices(ix0, iy0, ix1 - ix0, iy1 - iy0, surface_w, surface_h, color).to_vec()
+    quad_vertices(ix0, iy0, ix1 - ix0, iy1 - iy0, surface_w, surface_h, color, clip_circle).to_vec()
 }
 
-fn widget_vertices_clipped(w: &dyn Widget, sw: f32, sh: f32, clip: (f32, f32, f32, f32)) -> Vec<Vertex> {
+fn widget_vertices_clipped(w: &dyn Widget, sw: f32, sh: f32, clip: (f32, f32, f32, f32), clip_circle: [f32; 3]) -> Vec<Vertex> {
     let (x, y, ww, h) = w.rect();
-    quad_vertices_clipped(x, y, ww, h, sw, sh, w.color(), clip)
+    quad_vertices_clipped(x, y, ww, h, sw, sh, w.color(), clip, clip_circle)
+}
+
+fn circle_vertices(
+    cx: f32, cy: f32, r: f32,
+    sw: f32, sh: f32,
+    color: [f32; 4],
+    segments: usize,
+    clip_circle: [f32; 3],
+) -> Vec<Vertex> {
+    let mut verts = Vec::new();
+    for i in 0..segments {
+        let theta1 = (i as f32) * 2.0 * std::f32::consts::PI / (segments as f32);
+        let theta2 = ((i + 1) as f32) * 2.0 * std::f32::consts::PI / (segments as f32);
+        let x0 = cx;
+        let y0 = cy;
+        let x1 = cx + r * theta1.cos();
+        let y1 = cy + r * theta1.sin();
+        let x2 = cx + r * theta2.cos();
+        let y2 = cy + r * theta2.sin();
+        
+        let ndc_x0 = (x0 / sw) * 2.0 - 1.0;
+        let ndc_y0 = 1.0 - (y0 / sh) * 2.0;
+        let ndc_x1 = (x1 / sw) * 2.0 - 1.0;
+        let ndc_y1 = 1.0 - (y1 / sh) * 2.0;
+        let ndc_x2 = (x2 / sw) * 2.0 - 1.0;
+        let ndc_y2 = 1.0 - (y2 / sh) * 2.0;
+        
+        verts.push(Vertex { position: [ndc_x0, ndc_y0], color, clip_circle });
+        verts.push(Vertex { position: [ndc_x1, ndc_y1], color, clip_circle });
+        verts.push(Vertex { position: [ndc_x2, ndc_y2], color, clip_circle });
+    }
+    verts
+}
+
+fn circle_border_vertices(
+    cx: f32, cy: f32, r: f32,
+    thickness: f32,
+    sw: f32, sh: f32,
+    color: [f32; 4],
+    segments: usize,
+    clip_circle: [f32; 3],
+) -> Vec<Vertex> {
+    let mut verts = Vec::new();
+    for i in 0..segments {
+        let theta1 = (i as f32) * 2.0 * std::f32::consts::PI / (segments as f32);
+        let theta2 = ((i + 1) as f32) * 2.0 * std::f32::consts::PI / (segments as f32);
+        
+        let x0 = cx + (r - thickness) * theta1.cos();
+        let y0 = cy + (r - thickness) * theta1.sin();
+        let x1 = cx + r * theta1.cos();
+        let y1 = cy + r * theta1.sin();
+        
+        let x2 = cx + r * theta2.cos();
+        let y2 = cy + r * theta2.sin();
+        let x3 = cx + (r - thickness) * theta2.cos();
+        let y3 = cy + (r - thickness) * theta2.sin();
+        
+        let ndc_x0 = (x0 / sw) * 2.0 - 1.0; let ndc_y0 = 1.0 - (y0 / sh) * 2.0;
+        let ndc_x1 = (x1 / sw) * 2.0 - 1.0; let ndc_y1 = 1.0 - (y1 / sh) * 2.0;
+        let ndc_x2 = (x2 / sw) * 2.0 - 1.0; let ndc_y2 = 1.0 - (y2 / sh) * 2.0;
+        let ndc_x3 = (x3 / sw) * 2.0 - 1.0; let ndc_y3 = 1.0 - (y3 / sh) * 2.0;
+        
+        verts.push(Vertex { position: [ndc_x0, ndc_y0], color, clip_circle });
+        verts.push(Vertex { position: [ndc_x1, ndc_y1], color, clip_circle });
+        verts.push(Vertex { position: [ndc_x2, ndc_y2], color, clip_circle });
+        
+        verts.push(Vertex { position: [ndc_x0, ndc_y0], color, clip_circle });
+        verts.push(Vertex { position: [ndc_x2, ndc_y2], color, clip_circle });
+        verts.push(Vertex { position: [ndc_x3, ndc_y3], color, clip_circle });
+    }
+    verts
+}
+
+fn arc_background_vertices(
+    cx: f32, cy: f32, r: f32,
+    thickness: f32,
+    start_angle: f32, end_angle: f32,
+    sw: f32, sh: f32,
+    color: [f32; 4],
+    segments: usize,
+    clip_circle: [f32; 3],
+) -> Vec<Vertex> {
+    let mut verts = Vec::new();
+    for i in 0..segments {
+        let theta1 = start_angle + (i as f32) * (end_angle - start_angle) / (segments as f32);
+        let theta2 = start_angle + ((i + 1) as f32) * (end_angle - start_angle) / (segments as f32);
+        
+        let x0 = cx + (r - thickness) * theta1.cos();
+        let y0 = cy + (r - thickness) * theta1.sin();
+        let x1 = cx + r * theta1.cos();
+        let y1 = cy + r * theta1.sin();
+        
+        let x2 = cx + r * theta2.cos();
+        let y2 = cy + r * theta2.sin();
+        let x3 = cx + (r - thickness) * theta2.cos();
+        let y3 = cy + (r - thickness) * theta2.sin();
+        
+        let ndc_x0 = (x0 / sw) * 2.0 - 1.0; let ndc_y0 = 1.0 - (y0 / sh) * 2.0;
+        let ndc_x1 = (x1 / sw) * 2.0 - 1.0; let ndc_y1 = 1.0 - (y1 / sh) * 2.0;
+        let ndc_x2 = (x2 / sw) * 2.0 - 1.0; let ndc_y2 = 1.0 - (y2 / sh) * 2.0;
+        let ndc_x3 = (x3 / sw) * 2.0 - 1.0; let ndc_y3 = 1.0 - (y3 / sh) * 2.0;
+        
+        verts.push(Vertex { position: [ndc_x0, ndc_y0], color, clip_circle });
+        verts.push(Vertex { position: [ndc_x1, ndc_y1], color, clip_circle });
+        verts.push(Vertex { position: [ndc_x2, ndc_y2], color, clip_circle });
+        
+        verts.push(Vertex { position: [ndc_x0, ndc_y0], color, clip_circle });
+        verts.push(Vertex { position: [ndc_x2, ndc_y2], color, clip_circle });
+        verts.push(Vertex { position: [ndc_x3, ndc_y3], color, clip_circle });
+    }
+    verts
 }
 
 fn make_text_buffer(font_system: &mut FontSystem, text: &str, size: f32) -> Buffer {
@@ -1649,6 +1848,7 @@ struct State {
     show_origin: bool,
     show_camera_pivot: bool,
     viewport_bg_color: [f32; 3],
+    node_color: [f32; 3],
     vertex_buffer_origin: wgpu::Buffer,
     vertex_count_origin: u32,
     vertex_buffer_pivot: wgpu::Buffer,
@@ -1668,10 +1868,8 @@ struct State {
 
     widgets: Vec<Box<dyn Widget>>,
     positions: Vec<(f32, f32, f32, f32)>,
-    node_slots: Vec<usize>,
     splitter1_x: f32,
     splitter2_x: f32,
-    left_offsets: Vec<(f32, f32)>,
     node_palette_visible: bool,
     node_palette_query: String,
     node_palette_filtered: Vec<usize>,
@@ -1734,6 +1932,16 @@ struct State {
     zoom_velocity: f32,
     grid_thickness: f32,
     focused_pane: usize,
+    inertial_scroll_enabled: bool,
+    inertial_scroll_friction: f32,
+    last_config_read: Instant,
+    circular_network_pane: bool,
+    network_circle_x: f32,
+    network_circle_y: f32,
+    network_circle_radius: f32,
+    is_dragging_network_circle: bool,
+    circle_drag_ox: f32,
+    circle_drag_oy: f32,
 }
 
 impl State {
@@ -1750,6 +1958,7 @@ impl State {
             show_origin_enabled: self.show_origin,
             origin_size: self.origin_size,
             viewport_bg_color: self.viewport_bg_color,
+            node_color: self.node_color,
             square_viewport: self.square_viewport,
             grid_thickness: self.grid_thickness,
             show_camera_pivot_enabled: self.show_camera_pivot,
@@ -1844,11 +2053,66 @@ impl State {
     }
 
     fn open_node_palette(&mut self) {
-        self.node_palette_visible = true;
-        self.node_palette_query.clear();
-        self.node_palette_selected = 0;
-        self.refresh_node_palette();
-        self.upload_vertices();
+        let templates: Vec<String> = self.node_templates.iter().map(|t| t.label.clone()).collect();
+        let grid_col = self.grid_cursor_col;
+        let grid_row = self.grid_cursor_row;
+        std::thread::spawn(move || {
+            use std::io::Write;
+            use std::process::{Command, Stdio};
+
+            let mut child = match Command::new("clear-cloud")
+                .arg("-p")
+                .arg("Add Node: ")
+                .arg("--dmenu")
+                .stdin(Stdio::piped())
+                .stdout(Stdio::piped())
+                .spawn()
+            {
+                Ok(c) => c,
+                Err(e) => {
+                    eprintln!("Failed to spawn clear-cloud: {:?}", e);
+                    return;
+                }
+            };
+
+            if let Some(mut stdin) = child.stdin.take() {
+                for t in &templates {
+                    let _ = writeln!(stdin, "{}", t);
+                }
+            }
+
+            let output = match child.wait_with_output() {
+                Ok(o) => o,
+                Err(e) => {
+                    eprintln!("Failed to wait for clear-cloud: {:?}", e);
+                    return;
+                }
+            };
+
+            if output.status.success() {
+                let selected = String::from_utf8_lossy(&output.stdout).trim().to_string();
+                if !selected.is_empty() {
+                    let body = format!(
+                        "{{\"action\":\"add_node\",\"template_name\":\"{}\",\"x\":{},\"y\":{}}}",
+                        selected, grid_col, grid_row
+                    );
+                    let req = format!(
+                        "POST /action HTTP/1.1\r\n\
+                         Host: 127.0.0.1:3000\r\n\
+                         Content-Type: application/json\r\n\
+                         Content-Length: {}\r\n\
+                         Connection: close\r\n\r\n\
+                         {}",
+                        body.len(),
+                        body
+                    );
+                    if let Ok(mut stream) = std::net::TcpStream::connect("127.0.0.1:3000") {
+                        let _ = stream.write_all(req.as_bytes());
+                        let _ = stream.flush();
+                    }
+                }
+            }
+        });
     }
 
     fn close_node_palette(&mut self) {
@@ -1860,13 +2124,10 @@ impl State {
 
     fn place_selected_node(&mut self) -> bool {
         let Some(&template_idx) = self.node_palette_filtered.get(self.node_palette_selected) else { return false; };
-        let child_idx = self.current_dir().children.len();
-        if child_idx >= self.node_slots.len() { return false; }
         let mut node = self.node_templates[template_idx].node.clone();
         let (nx, ny) = self.find_empty_cell(self.grid_cursor_col as f32, self.grid_cursor_row as f32, None);
         node.position = (nx, ny);
         self.current_dir_mut().children.push(node);
-        self.left_offsets[child_idx] = (nx, ny);
         self.close_node_palette();
         self.sync_nodes();
         self.rebuild_positions();
@@ -1876,6 +2137,7 @@ impl State {
         true
     }
 
+
     fn handle_node_palette_key(&mut self, event: &KeyEvent) -> bool {
         if event.state != ElementState::Pressed { return false; }
         match &event.logical_key {
@@ -2068,39 +2330,16 @@ fn geometry_to_spreadsheet_data(geom: &Geometry) -> (Vec<String>, Vec<Vec<String
     }
 
     fn sync_nodes(&mut self) {
-        let node_infos: Vec<Option<(String, (f32, f32))>> = {
-            let dir = self.current_dir();
-            self.node_slots.iter().enumerate().map(|(i, _)| {
-                dir.children.get(i).map(|c| (c.name.clone(), c.position))
-            }).collect()
-        };
-        let params_list: Vec<Vec<(String, String, String)>> = {
-            let dir = self.current_dir();
-            self.node_slots.iter().enumerate().map(|(i, _)| {
-                dir.children.get(i).map(|c| param_display(&c.params)).unwrap_or_default()
-            }).collect()
-        };
-        let geom_visibles: Vec<bool> = {
-            let dir = self.current_dir();
-            self.node_slots.iter().enumerate().map(|(i, _)| {
-                dir.children.get(i).map(|c| c.geometry_visible).unwrap_or(true)
-            }).collect()
-        };
-
-        for (i, &slot) in self.node_slots.iter().enumerate() {
-            if let Some((name, position)) = &node_infos[i] {
-                if i < self.left_offsets.len() {
-                    self.left_offsets[i] = *position;
-                }
-                self.widgets[slot].set_node_name(name);
-                self.widgets[slot].set_display_params(&params_list[i]);
-                self.widgets[slot].set_geom_visible(geom_visibles[i]);
-            } else {
-                self.widgets[slot].set_node_name("");
-                self.widgets[slot].set_display_params(&[]);
-                self.widgets[slot].set_geom_visible(true);
+        let graph_nodes: Vec<GraphNode> = self.current_dir().children.iter().map(|c| {
+            GraphNode {
+                name: c.name.clone(),
+                position: c.position,
+                parameters: param_display(&c.params),
+                geom_visible: c.geometry_visible,
             }
-        }
+        }).collect();
+        self.widgets[CONTENT_IDX].set_nodes(&graph_nodes);
+
         let camera_nodes: Vec<String> = self.current_dir().children.iter()
             .filter(|c| c.node_type == "camera")
             .map(|c| c.name.clone())
@@ -2119,17 +2358,16 @@ fn geometry_to_spreadsheet_data(geom: &Geometry) -> (Vec<String>, Vec<Vec<String
         self.widgets[BREADCRUMB_IDX].set_path(&path_strs);
 
         let mut selected_node = None;
-        if let Some(focused) = self.focused_widget {
-            if self.node_slots.contains(&focused) {
-                if let Some(slot_idx) = self.node_slots.iter().position(|&x| x == focused) {
-                    let dir = self.current_dir();
-                    if slot_idx < dir.children.len() {
-                        selected_node = Some(&dir.children[slot_idx]);
-                    }
+        if self.focused_widget == Some(CONTENT_IDX) {
+            if let Some(slot_idx) = self.widgets[CONTENT_IDX].selected_node() {
+                let dir = self.current_dir();
+                if slot_idx < dir.children.len() {
+                    selected_node = Some(&dir.children[slot_idx]);
                 }
             }
         }
 
+
         let mut cache_hit = false;
         let mut current_name = None;
         let mut current_params = None;
@@ -2295,7 +2533,7 @@ fn geometry_to_spreadsheet_data(geom: &Geometry) -> (Vec<String>, Vec<Vec<String
         let window = xdg_shell_state.create_window(wl_surface.clone(), WindowDecorations::None, qh);
         window.set_title("Clear Design Interface");
         window.set_app_id("clear-design-interface");
-        window.set_min_size(Some((pw, ph)));
+        window.set_min_size(Some(((480.0 * scale) as u32, (320.0 * scale) as u32)));
         window.commit();
 
         let wayland_handle = Box::leak(Box::new(clear_ui::wayland::WaylandSurfaceHandle {
@@ -2618,31 +2856,23 @@ fn geometry_to_spreadsheet_data(geom: &Geometry) -> (Vec<String>, Vec<Vec<String
         } else {
             vec![]
         };
-        let node_slots: Vec<usize> = (NODE_SLOT_START..NODE_SLOT_START + NODE_SLOT_COUNT).collect();
-        let left_offsets: Vec<(f32, f32)> = (0..NODE_SLOT_COUNT)
-            .map(|i| (0.0, i as f32))
-            .collect();
-
         let mut widgets: Vec<Box<dyn Widget>> = vec![
             Box::new(MenuBar::new(0.0, 0.0, 0.0, HEADER_H).with_title("Clear Design Interface").with_item("File", &["New Project", "Open", "Save", "Configure", "Exit"]).with_item("Edit", &["Undo", "Redo"]).with_item("View", &["Zoom In", "Zoom Out", "Reset Zoom"]).with_item("Help", &["About"]).with_z_index(110)),
-            Box::new(ContentBg::new()),
+            Box::new(Graph::new()),
             Box::new(Splitter::new(SPLITTER_W)),
             Box::new(ViewportBg::new()),
             Box::new(Splitter::new(SPLITTER_W)),
             Box::new(ParametersBg::new()),
             Box::new(Canvas::new()),
-            Box::new(MenuBar::new(0.0, 0.0, 0.0, MENUBAR_H).with_title("0: Network").with_item("File", &["New", "Open", "Save"]).with_item("Edit", &["Undo", "Redo"]).with_item("View", &["Zoom In", "Zoom Out"])),
+            Box::new(MenuBar::new(0.0, 0.0, 0.0, MENUBAR_H).with_title("0: Network").with_item("File", &["New", "Open", "Save"]).with_item("Edit", &["Undo", "Redo"]).with_item("View", &["Zoom In", "Zoom Out", "Circular Pane"])),
             Box::new(MenuBar::new(0.0, 0.0, 0.0, MENUBAR_H).with_title("1: Viewport").with_item("Camera", &["Perspective", "Orthographic"]).with_item("Display", &["Square Aspect"]).with_item("Guides", &["Show Grid", "Cube", "Origin", "Camera Pivot"])),
             Box::new(MenuBar::new(0.0, 0.0, 0.0, MENUBAR_H).with_title("2: Parameters").with_item("Preset", &["Default", "Custom"]).with_item("Reset", &["All"])),
+            Box::new(StatusBar::new()),
+            Box::new(Breadcrumb::new()),
+            Box::new(ConfigDialog::new(&settings)),
+            Box::new(NodePalette::new()),
+            Box::new(Spreadsheet::new()),
         ];
-        for _ in 0..NODE_SLOT_COUNT {
-            widgets.push(Box::new(Node::new(0.0, 0.0, 0.0, 0.0, "")));
-        }
-        widgets.push(Box::new(StatusBar::new()));
-        widgets.push(Box::new(Breadcrumb::new()));
-        widgets.push(Box::new(ConfigDialog::new(&settings)));
-        widgets.push(Box::new(NodePalette::new()));
-        widgets.push(Box::new(Spreadsheet::new()));
         
         let mut spreadsheet_menubar = MenuBar::new(0.0, 0.0, 0.0, MENUBAR_H).with_title("3: Spreadsheet");
         spreadsheet_menubar.visible = false;
@@ -2698,6 +2928,7 @@ fn geometry_to_spreadsheet_data(geom: &Geometry) -> (Vec<String>, Vec<Vec<String
             show_origin: settings.show_origin_enabled,
             show_camera_pivot: settings.show_camera_pivot_enabled,
             viewport_bg_color: settings.viewport_bg_color,
+            node_color: settings.node_color,
             vertex_buffer_origin,
             vertex_count_origin,
             vertex_buffer_pivot,
@@ -2714,10 +2945,8 @@ fn geometry_to_spreadsheet_data(geom: &Geometry) -> (Vec<String>, Vec<Vec<String
             exit_requested: false,
             widgets,
             positions,
-            node_slots,
             splitter1_x,
             splitter2_x,
-            left_offsets,
             node_palette_visible: false,
             node_palette_query: String::new(),
             node_palette_filtered: Vec::new(),
@@ -2775,8 +3004,25 @@ fn geometry_to_spreadsheet_data(geom: &Geometry) -> (Vec<String>, Vec<Vec<String
             zoom_velocity: 0.0,
             grid_thickness: settings.grid_thickness,
             focused_pane: LEFT_MENUBAR_IDX,
+            inertial_scroll_enabled: true,
+            inertial_scroll_friction: 0.90,
+            last_config_read: Instant::now(),
+            circular_network_pane: false,
+            network_circle_x: 250.0,
+            network_circle_y: 300.0,
+            network_circle_radius: 180.0,
+            is_dragging_network_circle: false,
+            circle_drag_ox: 0.0,
+            circle_drag_oy: 0.0,
         };
 
+        state.update_inertial_settings();
+        colors::set_node_color([
+            settings.node_color[0],
+            settings.node_color[1],
+            settings.node_color[2],
+            1.0,
+        ]);
         state.sync_nodes();
         state.rebuild_scene_geometry();
         state.sync_grid_settings();
@@ -2784,6 +3030,7 @@ fn geometry_to_spreadsheet_data(geom: &Geometry) -> (Vec<String>, Vec<Vec<String
         state.widgets[RIGHT_MENUBAR_IDX].set_item_checked(2, 1, state.show_cube);
         state.widgets[RIGHT_MENUBAR_IDX].set_item_checked(2, 2, state.show_origin);
         state.widgets[RIGHT_MENUBAR_IDX].set_item_checked(2, 3, state.show_camera_pivot);
+        state.widgets[LEFT_MENUBAR_IDX].set_item_checked(2, 2, state.circular_network_pane);
 
         state.rebuild_positions();
         state.apply_layout();
@@ -2795,19 +3042,55 @@ fn geometry_to_spreadsheet_data(geom: &Geometry) -> (Vec<String>, Vec<Vec<String
 
     fn sync_grid_settings(&mut self) {
         let node_area_y = HEADER_H + MENUBAR_H + BREADCRUMB_H;
+        let active_node_area_y = if self.circular_network_pane {
+            self.network_circle_y - self.network_circle_radius + 45.0 + MENUBAR_H + BREADCRUMB_H
+        } else {
+            node_area_y
+        };
+        let active_node_area_x = if self.circular_network_pane {
+            self.network_circle_x - self.network_circle_radius
+        } else {
+            0.0
+        };
+
         self.widgets[CONTENT_IDX].set_show_network_grid(self.network_grid_visible);
         self.widgets[CONTENT_IDX].set_grid_sizes(self.grid_size_x, self.grid_size_y);
         self.widgets[CONTENT_IDX].set_skipped_sizes(self.skipped_row_h, self.skipped_col_w);
-        self.widgets[CONTENT_IDX].set_grid_origin(self.pan_x, node_area_y + self.pan_y);
+        self.widgets[CONTENT_IDX].set_grid_origin(active_node_area_x + self.pan_x, active_node_area_y + self.pan_y);
+        self.widgets[CONTENT_IDX].set_grid_snap_enabled(self.grid_snap_enabled);
+    }
 
-        for &i in &self.node_slots {
-            let gx = if self.grid_snap_enabled { self.grid_size_x + self.skipped_col_w } else { 0.0 };
-            let gy = if self.grid_snap_enabled { self.grid_size_y + self.skipped_row_h } else { 0.0 };
-            self.widgets[i].set_grid_snap(gx, gy);
-            self.widgets[i].set_grid_origin(self.pan_x, node_area_y + self.pan_y);
+    fn update_inertial_settings(&mut self) {
+        self.last_config_read = Instant::now();
+        let config_path = "/home/lsgalante/.config/ccec/config.toml";
+        if let Ok(content) = std::fs::read_to_string(config_path) {
+            #[derive(serde::Deserialize)]
+            struct InertialSection {
+                inertial_scroll: Option<bool>,
+                scroll_friction: Option<u16>,
+            }
+            #[derive(serde::Deserialize)]
+            struct Config {
+                inertial: Option<InertialSection>,
+            }
+            if let Ok(cfg) = toml::from_str::<Config>(&content) {
+                if let Some(inertial) = cfg.inertial {
+                    if let Some(val) = inertial.inertial_scroll {
+                        self.inertial_scroll_enabled = val;
+                    }
+                    if let Some(friction_val) = inertial.scroll_friction {
+                        let friction_f = (friction_val as f32 / 1000.0).clamp(0.1, 0.999);
+                        self.inertial_scroll_friction = friction_f;
+                    }
+                }
+            }
         }
+        // Defaults if file read or parsing fails
+        self.inertial_scroll_enabled = true;
+        self.inertial_scroll_friction = 0.90;
     }
 
+
     fn zoom(&mut self, factor: f32, center: Option<(f32, f32)>) {
         self.pan_velocity_x = 0.0;
         self.pan_velocity_y = 0.0;
@@ -2852,37 +3135,55 @@ fn geometry_to_spreadsheet_data(geom: &Geometry) -> (Vec<String>, Vec<Vec<String
         self.skipped_col_w = old_col_w * (new_gx / old_gx);
 
         self.sync_grid_settings();
-        for &i in &self.node_slots {
-            let (x, y, _, _) = self.widgets[i].rect();
-            self.widgets[i].set_rect(x, y, self.grid_size_x, self.grid_size_y);
-        }
     }
 
+
     fn keep_cursor_in_view(&mut self) {
         let node_area_y = HEADER_H + MENUBAR_H + BREADCRUMB_H;
-        let cx = self.grid_cursor_col as f32 * (self.grid_size_x + self.skipped_col_w) + self.pan_x;
-        let cy = node_area_y + self.grid_cursor_row as f32 * (self.grid_size_y + self.skipped_row_h) + self.pan_y;
+        let active_node_area_y = if self.circular_network_pane {
+            self.network_circle_y - self.network_circle_radius + 45.0 + MENUBAR_H + BREADCRUMB_H
+        } else {
+            node_area_y
+        };
+        let active_node_area_x = if self.circular_network_pane {
+            self.network_circle_x - self.network_circle_radius
+        } else {
+            0.0
+        };
+
+        let cx = active_node_area_x + self.grid_cursor_col as f32 * (self.grid_size_x + self.skipped_col_w) + self.pan_x;
+        let cy = active_node_area_y + self.grid_cursor_row as f32 * (self.grid_size_y + self.skipped_row_h) + self.pan_y;
         let cw = self.grid_size_x;
         let ch = self.grid_size_y;
-        let clw = self.content_left_w();
-        let max_y = self.height - STATUS_H;
 
-        if cx < 0.0 {
-            self.pan_x -= cx;
-        } else if cx + cw > clw {
-            self.pan_x -= cx + cw - clw;
+        let active_graph_w = if self.circular_network_pane {
+            2.0 * self.network_circle_radius
+        } else {
+            self.content_left_w()
+        };
+        let active_max_y = if self.circular_network_pane {
+            self.network_circle_y + self.network_circle_radius
+        } else {
+            self.height - STATUS_H
+        };
+
+        if cx < active_node_area_x {
+            self.pan_x -= cx - active_node_area_x;
+        } else if cx + cw > active_node_area_x + active_graph_w {
+            self.pan_x -= cx + cw - (active_node_area_x + active_graph_w);
         }
 
-        if cy < node_area_y {
-            self.pan_y -= cy - node_area_y;
-        } else if cy + ch > max_y {
-            self.pan_y -= cy + ch - max_y;
+        if cy < active_node_area_y {
+            self.pan_y -= cy - active_node_area_y;
+        } else if cy + ch > active_max_y {
+            self.pan_y -= cy + ch - active_max_y;
         }
         self.sync_grid_settings();
     }
 
     fn rebuild_positions(&mut self) {
         self.clamp_splitters();
+        self.widgets[LEFT_MENUBAR_IDX].set_center_items(self.circular_network_pane);
 
         let body_h = self.body_h();
         let clw = self.content_left_w();
@@ -2890,52 +3191,66 @@ fn geometry_to_spreadsheet_data(geom: &Geometry) -> (Vec<String>, Vec<Vec<String
         let vw = self.viewport_w();
         let px = self.param_x();
 
+        let node_area_y = HEADER_H + MENUBAR_H;
         self.positions[0] = (0.0, 0.0, self.width, HEADER_H);
-        self.positions[CONTENT_IDX] = (0.0, HEADER_H, clw, body_h);
-        self.positions[SPLITTER1_IDX] = (self.splitter1_x, HEADER_H, SPLITTER_W, body_h);
-        self.positions[SPLITTER2_IDX] = (self.splitter2_x, HEADER_H, SPLITTER_W, body_h);
-        self.positions[PARAM_IDX] = (px, HEADER_H, self.param_w(), body_h);
 
-        self.positions[VIEWPORT_IDX] = (crx, HEADER_H, vw, body_h);
-        self.positions[CANVAS_IDX] = (0.0, HEADER_H, self.width, body_h);
-
-        if self.show_spreadsheet {
-            let viewport_h = body_h * 2.0 / 3.0;
-            let spreadsheet_h = body_h - viewport_h;
-
-            self.positions[SPREADSHEET_MENUBAR_IDX] = (crx, HEADER_H + viewport_h, vw, MENUBAR_H);
-            self.positions[SPREADSHEET_IDX] = (crx, HEADER_H + viewport_h + MENUBAR_H, vw, spreadsheet_h - MENUBAR_H);
+        if self.circular_network_pane {
+            let cx = self.network_circle_x;
+            let cy = self.network_circle_y;
+            let r = self.network_circle_radius;
+
+            self.positions[LEFT_MENUBAR_IDX] = (cx - r, cy - r, 2.0 * r, 35.0);
+            self.widgets[LEFT_MENUBAR_IDX].set_curved_circle(Some((cx, cy, r)));
+            self.positions[BREADCRUMB_IDX] = (cx - r, cy - r + 45.0 + MENUBAR_H, 2.0 * r, BREADCRUMB_H);
+            self.positions[CONTENT_IDX] = (cx - r, cy - r + 45.0 + MENUBAR_H + BREADCRUMB_H, 2.0 * r, 2.0 * r - (45.0 + MENUBAR_H + BREADCRUMB_H));
+            self.positions[SPLITTER1_IDX] = (0.0, 0.0, 0.0, 0.0);
+            self.positions[SPLITTER2_IDX] = (self.splitter2_x, HEADER_H, SPLITTER_W, body_h);
+            self.positions[PARAM_IDX] = (px, HEADER_H, self.param_w(), body_h);
+
+            let viewport_w = self.splitter2_x;
+            self.positions[VIEWPORT_IDX] = (0.0, HEADER_H, viewport_w, body_h);
+            self.positions[RIGHT_MENUBAR_IDX] = (0.0, HEADER_H, viewport_w, MENUBAR_H);
+
+            if self.show_spreadsheet {
+                let viewport_h = body_h * 2.0 / 3.0;
+                let spreadsheet_h = body_h - viewport_h;
+                self.positions[SPREADSHEET_MENUBAR_IDX] = (0.0, HEADER_H + viewport_h, viewport_w, MENUBAR_H);
+                self.positions[SPREADSHEET_IDX] = (0.0, HEADER_H + viewport_h + MENUBAR_H, viewport_w, spreadsheet_h - MENUBAR_H);
+            } else {
+                self.positions[SPREADSHEET_IDX] = (0.0, 0.0, 0.0, 0.0);
+                self.positions[SPREADSHEET_MENUBAR_IDX] = (0.0, 0.0, 0.0, 0.0);
+            }
         } else {
-            self.positions[SPREADSHEET_IDX] = (0.0, 0.0, 0.0, 0.0);
-            self.positions[SPREADSHEET_MENUBAR_IDX] = (0.0, 0.0, 0.0, 0.0);
-        }
-
-        self.positions[LEFT_MENUBAR_IDX] = (0.0, HEADER_H, clw, MENUBAR_H);
-        self.positions[RIGHT_MENUBAR_IDX] = (crx, HEADER_H, vw, MENUBAR_H);
-        self.positions[PARAM_MENUBAR_IDX] = (px, HEADER_H, self.param_w(), MENUBAR_H);
-
-        let node_area_y = HEADER_H + MENUBAR_H;
-        self.positions[BREADCRUMB_IDX] = (0.0, node_area_y, clw, BREADCRUMB_H);
-
-        let active_nodes = self.current_dir().children.len().min(self.node_slots.len());
-        for (slot_idx, (&idx, off)) in self.node_slots.iter().zip(self.left_offsets.iter()).enumerate() {
-            if slot_idx < active_nodes {
-                self.positions[idx] = (
-                    off.0 * (self.grid_size_x + self.skipped_col_w) + self.pan_x,
-                    node_area_y + BREADCRUMB_H + off.1 * (self.grid_size_y + self.skipped_row_h) + self.pan_y,
-                    self.grid_size_x,
-                    self.grid_size_y,
-                );
+            self.widgets[LEFT_MENUBAR_IDX].set_curved_circle(None);
+            self.positions[CONTENT_IDX] = (0.0, node_area_y + BREADCRUMB_H, clw, self.height - STATUS_H - (node_area_y + BREADCRUMB_H));
+            self.positions[SPLITTER1_IDX] = (self.splitter1_x, HEADER_H, SPLITTER_W, body_h);
+            self.positions[SPLITTER2_IDX] = (self.splitter2_x, HEADER_H, SPLITTER_W, body_h);
+            self.positions[PARAM_IDX] = (px, HEADER_H, self.param_w(), body_h);
+
+            self.positions[VIEWPORT_IDX] = (crx, HEADER_H, vw, body_h);
+            self.positions[RIGHT_MENUBAR_IDX] = (crx, HEADER_H, vw, MENUBAR_H);
+            self.positions[BREADCRUMB_IDX] = (0.0, node_area_y, clw, BREADCRUMB_H);
+            self.positions[LEFT_MENUBAR_IDX] = (0.0, HEADER_H, clw, MENUBAR_H);
+
+            if self.show_spreadsheet {
+                let viewport_h = body_h * 2.0 / 3.0;
+                let spreadsheet_h = body_h - viewport_h;
+                self.positions[SPREADSHEET_MENUBAR_IDX] = (crx, HEADER_H + viewport_h, vw, MENUBAR_H);
+                self.positions[SPREADSHEET_IDX] = (crx, HEADER_H + viewport_h + MENUBAR_H, vw, spreadsheet_h - MENUBAR_H);
             } else {
-                self.positions[idx] = (0.0, 0.0, 0.0, 0.0);
+                self.positions[SPREADSHEET_IDX] = (0.0, 0.0, 0.0, 0.0);
+                self.positions[SPREADSHEET_MENUBAR_IDX] = (0.0, 0.0, 0.0, 0.0);
             }
         }
 
+        self.positions[CANVAS_IDX] = (0.0, HEADER_H, self.width, body_h);
+        self.positions[PARAM_MENUBAR_IDX] = (px, HEADER_H, self.param_w(), MENUBAR_H);
         self.positions[STATUS_IDX] = (0.0, self.height - STATUS_H, self.width, STATUS_H);
         self.positions[CONFIG_DIALOG_IDX] = (0.0, 0.0, self.width, self.height);
         self.positions[NODE_PALETTE_IDX] = (0.0, 0.0, self.width, self.height);
     }
 
+
     fn apply_layout(&mut self) {
         for (i, pos) in self.positions.iter().enumerate() {
             if let Some(widget) = self.widgets.get_mut(i) {
@@ -2954,6 +3269,9 @@ fn geometry_to_spreadsheet_data(geom: &Geometry) -> (Vec<String>, Vec<Vec<String
         for &menubar_idx in &[LEFT_MENUBAR_IDX, RIGHT_MENUBAR_IDX, PARAM_MENUBAR_IDX, SPREADSHEET_MENUBAR_IDX] {
             self.widgets[menubar_idx].set_selected(menubar_idx == self.focused_pane);
         }
+        if self.focused_pane != PARAM_MENUBAR_IDX {
+            self.widgets[PARAM_IDX].unfocus();
+        }
     }
 
     fn sync_layout(&mut self) {
@@ -3029,6 +3347,13 @@ fn geometry_to_spreadsheet_data(geom: &Geometry) -> (Vec<String>, Vec<Vec<String
                 self.sync_pane_focus();
                 self.sync_nodes();
             }
+            Action::ToggleCircularPane => {
+                self.circular_network_pane = !self.circular_network_pane;
+                self.widgets[LEFT_MENUBAR_IDX].set_item_checked(2, 2, self.circular_network_pane);
+                self.rebuild_positions();
+                self.apply_layout();
+                self.sync_grid_settings();
+            }
         }
         if settings_changed {
             self.save_settings();
@@ -3036,63 +3361,50 @@ fn geometry_to_spreadsheet_data(geom: &Geometry) -> (Vec<String>, Vec<Vec<String
     }
 
     fn read_panel_offsets(&mut self) {
-        let node_area_y = HEADER_H + MENUBAR_H + BREADCRUMB_H;
-        let active_nodes = self.current_dir().children.len().min(self.node_slots.len());
-        let mut coords = Vec::with_capacity(active_nodes);
-        for (&idx, off) in self.node_slots.iter().take(active_nodes).zip(self.left_offsets.iter_mut()) {
-            let (px, py, _, _) = self.widgets[idx].rect();
-            let c = ((px - self.pan_x) / (self.grid_size_x + self.skipped_col_w)).round();
-            let r = ((py - node_area_y - self.pan_y) / (self.grid_size_y + self.skipped_row_h)).round();
-            *off = (c, r);
-            coords.push((c, r));
-            if Some(idx) == self.focused_widget && self.drag_widget == Some(idx) {
-                self.grid_cursor_col = c as i32;
-                self.grid_cursor_row = r as i32;
-            }
-        }
+        let updated_nodes = self.widgets[CONTENT_IDX].get_nodes();
         let dir = self.current_dir_mut();
-        for (i, coord) in coords.into_iter().enumerate() {
+        for (i, node) in updated_nodes.iter().enumerate() {
             if let Some(child) = dir.children.get_mut(i) {
-                child.position = coord;
+                child.position = node.position;
+            }
+        }
+        if let Some(sel_idx) = self.widgets[CONTENT_IDX].selected_node() {
+            if let Some(node) = updated_nodes.get(sel_idx) {
+                self.grid_cursor_col = node.position.0 as i32;
+                self.grid_cursor_row = node.position.1 as i32;
             }
         }
     }
 
     fn sync_cursor_and_selection(&mut self) {
         if let Some(old) = self.focused_widget {
-            if !self.node_slots.contains(&old) && old != SPREADSHEET_IDX {
+            if old != CONTENT_IDX && old != SPREADSHEET_IDX {
                 return;
             }
         }
-        let active_nodes = self.current_dir().children.len().min(self.node_slots.len());
-        let mut node_at_cursor = None;
-        for (slot_idx, (&idx, off)) in self.node_slots.iter().zip(self.left_offsets.iter()).enumerate() {
-            if slot_idx < active_nodes {
-                if off.0 as i32 == self.grid_cursor_col && off.1 as i32 == self.grid_cursor_row {
-                    node_at_cursor = Some(idx);
-                    break;
-                }
+        let dir = self.current_dir();
+        let mut node_at_cursor_idx = None;
+        for (i, node) in dir.children.iter().enumerate() {
+            if node.position.0 as i32 == self.grid_cursor_col && node.position.1 as i32 == self.grid_cursor_row {
+                node_at_cursor_idx = Some(i);
+                break;
             }
         }
 
-        if let Some(idx) = node_at_cursor {
-            if self.focused_widget != Some(idx) {
-                if let Some(old) = self.focused_widget {
-                    self.widgets[old].unfocus();
-                }
-                self.widgets[idx].focus();
-                self.focused_widget = Some(idx);
+        if let Some(idx) = node_at_cursor_idx {
+            self.widgets[CONTENT_IDX].set_selected_node(Some(idx));
+            if self.focused_widget != Some(CONTENT_IDX) {
+                self.focused_widget = Some(CONTENT_IDX);
             }
         } else {
-            if let Some(old) = self.focused_widget {
-                if self.node_slots.contains(&old) {
-                    self.widgets[old].unfocus();
-                    self.focused_widget = None;
-                }
+            self.widgets[CONTENT_IDX].set_selected_node(None);
+            if self.focused_widget == Some(CONTENT_IDX) {
+                self.focused_widget = None;
             }
         }
     }
 
+
     fn collect_vertices(&self) -> Vec<Vertex> {
         let sw = self.width;
         let sh = self.height;
@@ -3103,33 +3415,140 @@ fn geometry_to_spreadsheet_data(geom: &Geometry) -> (Vec<String>, Vec<Vec<String
         let show_cursor = self.drag_widget.is_none()
             && !dialog_open;
 
-        let clip = (0.0, node_area_y, self.content_left_w(), self.height - STATUS_H);
+        let clip = if self.circular_network_pane {
+            (
+                self.network_circle_x - self.network_circle_radius,
+                self.network_circle_y - self.network_circle_radius,
+                self.network_circle_x + self.network_circle_radius,
+                self.network_circle_y + self.network_circle_radius,
+            )
+        } else {
+            (0.0, node_area_y, self.content_left_w(), self.height - STATUS_H)
+        };
+
+        let clip_circle_val = if self.circular_network_pane {
+            [self.network_circle_x * self.scale as f32, self.network_circle_y * self.scale as f32, self.network_circle_radius * self.scale as f32]
+        } else {
+            [0.0, 0.0, 0.0]
+        };
 
         let mut draw_order: Vec<usize> = (0..self.widgets.len()).collect();
         draw_order.sort_by_key(|&i| self.widgets[i].z_index());
 
         for &i in &draw_order {
             let w = &self.widgets[i];
+            let is_network_part = i == CONTENT_IDX || i == LEFT_MENUBAR_IDX || i == BREADCRUMB_IDX;
+            let active_clip_circle = if is_network_part { clip_circle_val } else { [0.0, 0.0, 0.0] };
+
             if i == CONTENT_IDX {
-                verts.extend(widget_vertices(w.as_ref(), sw, sh));
+                if self.circular_network_pane {
+                    verts.extend(circle_vertices(
+                        self.network_circle_x,
+                        self.network_circle_y,
+                        self.network_circle_radius,
+                        sw,
+                        sh,
+                        [0.10, 0.10, 0.13, 0.95],
+                        64,
+                        active_clip_circle,
+                    ));
+                } else {
+                    verts.extend(widget_vertices(w.as_ref(), sw, sh, active_clip_circle));
+                }
+
                 for (qx, qy, qw, qh, qc) in w.extra_quads() {
-                    verts.extend(quad_vertices_clipped(qx, qy, qw, qh, sw, sh, qc, clip));
+                    verts.extend(quad_vertices_clipped(qx, qy, qw, qh, sw, sh, qc, clip, active_clip_circle));
+                }
+
+                if self.circular_network_pane {
+                    verts.extend(circle_border_vertices(
+                        self.network_circle_x,
+                        self.network_circle_y,
+                        self.network_circle_radius,
+                        3.0,
+                        sw,
+                        sh,
+                        [0.35, 0.65, 0.95, 0.80],
+                        64,
+                        active_clip_circle,
+                    ));
                 }
-            } else if self.node_slots.contains(&i) {
-                verts.extend(widget_vertices_clipped(w.as_ref(), sw, sh, clip));
+            } else if i == LEFT_MENUBAR_IDX && self.circular_network_pane {
+                let cx = self.network_circle_x;
+                let cy = self.network_circle_y;
+                let r = self.network_circle_radius;
+                
+                let bg_color = w.color();
+                verts.extend(arc_background_vertices(
+                    cx, cy, r,
+                    MENUBAR_H,
+                    std::f32::consts::PI,
+                    2.0 * std::f32::consts::PI,
+                    sw, sh,
+                    bg_color,
+                    64,
+                    active_clip_circle,
+                ));
+                
+                let border_color = [0.22, 0.22, 0.28, 0.90];
+                verts.extend(arc_background_vertices(
+                    cx, cy, r - MENUBAR_H,
+                    1.5,
+                    std::f32::consts::PI,
+                    2.0 * std::f32::consts::PI,
+                    sw, sh,
+                    border_color,
+                    64,
+                    active_clip_circle,
+                ));
+                
                 for (qx, qy, qw, qh, qc) in w.extra_quads() {
-                    verts.extend(quad_vertices_clipped(qx, qy, qw, qh, sw, sh, qc, clip));
+                    verts.extend(quad_vertices(qx, qy, qw, qh, sw, sh, qc, active_clip_circle));
+                }
+                for (acx, acy, ar, ath, a_start, a_end, acolor) in w.extra_arcs() {
+                    verts.extend(arc_background_vertices(
+                        acx, acy, ar,
+                        ath,
+                        a_start, a_end,
+                        sw, sh,
+                        acolor,
+                        64,
+                        active_clip_circle,
+                    ));
                 }
             } else {
-                verts.extend(widget_vertices(w.as_ref(), sw, sh));
+                verts.extend(widget_vertices(w.as_ref(), sw, sh, active_clip_circle));
                 for (qx, qy, qw, qh, qc) in w.extra_quads() {
-                    verts.extend(quad_vertices(qx, qy, qw, qh, sw, sh, qc));
+                    verts.extend(quad_vertices(qx, qy, qw, qh, sw, sh, qc, active_clip_circle));
+                }
+                for (acx, acy, ar, ath, a_start, a_end, acolor) in w.extra_arcs() {
+                    verts.extend(arc_background_vertices(
+                        acx, acy, ar,
+                        ath,
+                        a_start, a_end,
+                        sw, sh,
+                        acolor,
+                        64,
+                        active_clip_circle,
+                    ));
                 }
             }
 
+
             if i == CONTENT_IDX && show_cursor {
-                let cx = self.grid_cursor_col as f32 * (self.grid_size_x + self.skipped_col_w) + self.pan_x;
-                let cy = node_area_y + self.grid_cursor_row as f32 * (self.grid_size_y + self.skipped_row_h) + self.pan_y;
+                let cx = active_clip_circle[0] / self.scale as f32 - self.network_circle_radius + self.pan_x; // Wait, let's keep the exact cursor coordinates!
+                let active_node_area_y = if self.circular_network_pane {
+                    self.network_circle_y - self.network_circle_radius + 45.0 + MENUBAR_H + BREADCRUMB_H
+                } else {
+                    node_area_y
+                };
+                let active_node_area_x = if self.circular_network_pane {
+                    self.network_circle_x - self.network_circle_radius
+                } else {
+                    0.0
+                };
+                let cx = active_node_area_x + self.grid_cursor_col as f32 * (self.grid_size_x + self.skipped_col_w) + self.pan_x;
+                let cy = active_node_area_y + self.grid_cursor_row as f32 * (self.grid_size_y + self.skipped_row_h) + self.pan_y;
                 let cw = self.grid_size_x;
                 let ch = self.grid_size_y;
 
@@ -3147,13 +3566,14 @@ fn geometry_to_spreadsheet_data(geom: &Geometry) -> (Vec<String>, Vec<Vec<String
                     sh,
                     bg_color,
                     clip,
+                    active_clip_circle,
                 ));
 
                 // 4 border edges
-                verts.extend(quad_vertices_clipped(cx, cy, cw, thickness, sw, sh, border_color, clip));
-                verts.extend(quad_vertices_clipped(cx, cy + ch - thickness, cw, thickness, sw, sh, border_color, clip));
-                verts.extend(quad_vertices_clipped(cx, cy + thickness, thickness, ch - 2.0 * thickness, sw, sh, border_color, clip));
-                verts.extend(quad_vertices_clipped(cx + cw - thickness, cy + thickness, thickness, ch - 2.0 * thickness, sw, sh, border_color, clip));
+                verts.extend(quad_vertices_clipped(cx, cy, cw, thickness, sw, sh, border_color, clip, active_clip_circle));
+                verts.extend(quad_vertices_clipped(cx, cy + ch - thickness, cw, thickness, sw, sh, border_color, clip, active_clip_circle));
+                verts.extend(quad_vertices_clipped(cx, cy + thickness, thickness, ch - 2.0 * thickness, sw, sh, border_color, clip, active_clip_circle));
+                verts.extend(quad_vertices_clipped(cx + cw - thickness, cy + thickness, thickness, ch - 2.0 * thickness, sw, sh, border_color, clip, active_clip_circle));
             }
         }
         verts
@@ -3239,7 +3659,7 @@ fn geometry_to_spreadsheet_data(geom: &Geometry) -> (Vec<String>, Vec<Vec<String
             ref mut font_system, ref mut text_atlas, ref mut text_viewport,
             ref mut swash_cache, ref status_buffer,
             physical_width, physical_height, scale,
-            ref widgets, ref node_slots, ..
+            ref widgets, ..
         } = self;
 
         let viewport = Resolution { width: *physical_width, height: *physical_height };
@@ -3260,22 +3680,41 @@ fn geometry_to_spreadsheet_data(geom: &Geometry) -> (Vec<String>, Vec<Vec<String
         let mut widget_labels: Vec<TextLabel> = Vec::new();
         let mut widget_is_node: Vec<bool> = Vec::new();
         for (i, w) in widgets.iter().enumerate() {
-            let is_node = node_slots.contains(&i);
+            let is_node = i == CONTENT_IDX;
+            let is_network_part = i == CONTENT_IDX || i == LEFT_MENUBAR_IDX || i == BREADCRUMB_IDX;
             for label in w.text_labels() {
+                if self.circular_network_pane && is_network_part && i != LEFT_MENUBAR_IDX {
+                    let dx = label.x - self.network_circle_x;
+                    let dy = label.y - self.network_circle_y;
+                    let dist_sq = dx * dx + dy * dy;
+                    if dist_sq > self.network_circle_radius * self.network_circle_radius {
+                        continue;
+                    }
+                }
                 widget_buffers.push(make_text_buffer(font_system, &label.text, label.font_size));
                 widget_labels.push(label);
                 widget_is_node.push(is_node);
             }
         }
 
+
         for ((buf, label), &is_node) in widget_buffers.iter().zip(widget_labels.iter()).zip(widget_is_node.iter()) {
             let bounds = if is_node {
                 let node_area_y = HEADER_H + MENUBAR_H + BREADCRUMB_H;
-                TextBounds {
-                    left: 0,
-                    top: (node_area_y * s) as i32,
-                    right: (splitter1_x * s) as i32,
-                    bottom: (((height - STATUS_H) * s) as i32).max(0),
+                if self.circular_network_pane {
+                    TextBounds {
+                        left: ((self.network_circle_x - self.network_circle_radius) * s) as i32,
+                        top: ((self.network_circle_y - self.network_circle_radius) * s) as i32,
+                        right: ((self.network_circle_x + self.network_circle_radius) * s) as i32,
+                        bottom: (((self.network_circle_y + self.network_circle_radius) * s) as i32).max(0),
+                    }
+                } else {
+                    TextBounds {
+                        left: 0,
+                        top: (node_area_y * s) as i32,
+                        right: (splitter1_x * s) as i32,
+                        bottom: (((height - STATUS_H) * s) as i32).max(0),
+                    }
                 }
             } else {
                 TextBounds {
@@ -3380,36 +3819,46 @@ fn geometry_to_spreadsheet_data(geom: &Geometry) -> (Vec<String>, Vec<Vec<String
                 }
 
                 let result = if handled {
-                    if let Some(focused) = self.focused_widget {
-                        if self.node_slots.contains(&focused) {
-                            if let Some(slot_idx) = self.node_slots.iter().position(|&x| x == focused) {
-                                let updated_params = self.widgets[PARAM_IDX].node_params();
-                                let dir = self.current_dir_mut();
-                                if let Some(child) = dir.children.get_mut(slot_idx) {
-                                    let mut param_changed = false;
-                                    for (u_name, u_val, _type) in &updated_params {
-                                        if let Some(p) = child.params.iter_mut().find(|p| p.name == *u_name) {
-                                            if p.default != *u_val {
-                                                p.default = u_val.clone();
-                                                param_changed = true;
-                                            }
+                    if self.focused_widget == Some(CONTENT_IDX) {
+                        if let Some(slot_idx) = self.widgets[CONTENT_IDX].selected_node() {
+                            let updated_params = self.widgets[PARAM_IDX].node_params();
+                            let dir = self.current_dir_mut();
+                            if let Some(child) = dir.children.get_mut(slot_idx) {
+                                let mut param_changed = false;
+                                for (u_name, u_val, _type) in &updated_params {
+                                    if let Some(p) = child.params.iter_mut().find(|p| p.name == *u_name) {
+                                        if p.default != *u_val {
+                                            p.default = u_val.clone();
+                                            param_changed = true;
                                         }
                                     }
-                                    if param_changed {
-                                        self.rebuild_scene_geometry();
-                                        self.sync_nodes();
-                                    }
+                                }
+                                if param_changed {
+                                    self.rebuild_scene_geometry();
+                                    self.sync_nodes();
                                 }
                             }
                         }
                     }
 
                     // Sync Parameters pane with selected node
-                    let params = self.focused_widget.and_then(|f| {
-                        if self.node_slots.contains(&f) { Some(f) } else { None }
-                    }).map(|f| self.widgets[f].node_params()).unwrap_or_default();
+                    let params = if self.focused_widget == Some(CONTENT_IDX) {
+                        if let Some(slot_idx) = self.widgets[CONTENT_IDX].selected_node() {
+                            let dir = self.current_dir();
+                            if slot_idx < dir.children.len() {
+                                param_display(&dir.children[slot_idx].params)
+                            } else {
+                                vec![]
+                            }
+                        } else {
+                            vec![]
+                        }
+                    } else {
+                        vec![]
+                    };
                     self.widgets[PARAM_IDX].set_display_params(&params);
 
+
                     self.sync_layout();
                     self.read_panel_offsets();
                     self.upload_vertices();
@@ -3592,11 +4041,20 @@ fn geometry_to_spreadsheet_data(geom: &Geometry) -> (Vec<String>, Vec<Vec<String
                 }
             }
             WindowEvent::CursorMoved { position, .. } => {
-                self.cursor_x = position.x as f32 / self.scale as f32;
-                self.cursor_y = position.y as f32 / self.scale as f32;
+                self.cursor_x = position.x as f32;
+                self.cursor_y = position.y as f32;
+                println!("DEBUG: CursorMoved position=({:?}) scale={} => ({}, {})", position, self.scale, self.cursor_x, self.cursor_y);
+                let _ = std::io::Write::flush(&mut std::io::stdout());
                 let mut changed = false;
 
-                if self.is_panning {
+                if self.is_dragging_network_circle {
+                    self.network_circle_x = self.cursor_x - self.circle_drag_ox;
+                    self.network_circle_y = self.cursor_y - self.circle_drag_oy;
+                    self.rebuild_positions();
+                    self.apply_layout();
+                    self.sync_grid_settings();
+                    changed = true;
+                } else if self.is_panning {
                     let dx = self.cursor_x - self.pan_start_cx;
                     let dy = self.cursor_y - self.pan_start_cy;
                     self.pan_x = self.pan_start_x + dx;
@@ -3614,8 +4072,29 @@ fn geometry_to_spreadsheet_data(geom: &Geometry) -> (Vec<String>, Vec<Vec<String
                     }
 
                     if self.drag_widget.is_none() {
-                        for w in &mut self.widgets {
-                            if w.cursor_moved(self.cursor_x, self.cursor_y) {
+                        for i in 0..self.widgets.len() {
+                            let (cx, cy) = (self.cursor_x, self.cursor_y);
+                            let is_network_part = i == CONTENT_IDX || i == LEFT_MENUBAR_IDX || i == BREADCRUMB_IDX;
+                            let inside = if self.circular_network_pane && is_network_part {
+                                let cx_c = self.network_circle_x;
+                                let cy_c = self.network_circle_y;
+                                let r = self.network_circle_radius;
+                                if i == CONTENT_IDX {
+                                    let dx = cx - cx_c;
+                                    let dy = cy - cy_c;
+                                    dx * dx + dy * dy <= r * r && cy >= cy_c - r + 45.0 + MENUBAR_H + BREADCRUMB_H
+                                } else if i == LEFT_MENUBAR_IDX {
+                                    self.widgets[LEFT_MENUBAR_IDX].hit_test(cx, cy)
+                                } else if i == BREADCRUMB_IDX {
+                                    cx >= cx_c - r && cx <= cx_c + r && cy >= cy_c - r + 45.0 + MENUBAR_H && cy <= cy_c - r + 45.0 + MENUBAR_H + BREADCRUMB_H
+                                } else {
+                                    false
+                                }
+                            } else {
+                                self.widgets[i].hit_test(cx, cy)
+                            };
+                            let (tx, ty) = if inside { (cx, cy) } else { (-9999.0, -9999.0) };
+                            if self.widgets[i].cursor_moved(tx, ty) {
                                 changed = true;
                             }
                         }
@@ -3624,6 +4103,7 @@ fn geometry_to_spreadsheet_data(geom: &Geometry) -> (Vec<String>, Vec<Vec<String
                 changed
             }
             WindowEvent::MouseInput { state: btn_state, button, .. } => {
+                println!("DEBUG: MouseInput state={:?} button={:?} cursor=({}, {})", btn_state, button, self.cursor_x, self.cursor_y);
                 if *btn_state == ElementState::Pressed {
                     self.pan_velocity_x = 0.0;
                     self.pan_velocity_y = 0.0;
@@ -3652,7 +4132,6 @@ fn geometry_to_spreadsheet_data(geom: &Geometry) -> (Vec<String>, Vec<Vec<String
                 let is_pan_trigger = !dialog_open
                     && in_network_pane
                     && (*button == MouseButton::Middle
-                        || *button == MouseButton::Right
                         || (*button == MouseButton::Left && self.space_pressed));
 
                 if is_pan_trigger {
@@ -3689,34 +4168,110 @@ fn geometry_to_spreadsheet_data(geom: &Geometry) -> (Vec<String>, Vec<Vec<String
                     return true;
                 }
 
-                if *button != MouseButton::Left { return false; }
+                if *button != MouseButton::Left && *button != MouseButton::Right { return false; }
                 let mut changed = false;
                 let old_focus = self.focused_widget;
 
+                let hits_widget = |state: &State, i: usize, x: f32, y: f32| -> bool {
+                    if state.circular_network_pane && (i == CONTENT_IDX || i == LEFT_MENUBAR_IDX || i == BREADCRUMB_IDX) {
+                        let cx_c = state.network_circle_x;
+                        let cy_c = state.network_circle_y;
+                        let r = state.network_circle_radius;
+                        if i == CONTENT_IDX {
+                            let dx = x - cx_c;
+                            let dy = y - cy_c;
+                            dx * dx + dy * dy <= r * r && y >= cy_c - r + 45.0 + MENUBAR_H + BREADCRUMB_H
+                        } else if i == LEFT_MENUBAR_IDX {
+                            state.widgets[LEFT_MENUBAR_IDX].hit_test(x, y)
+                        } else if i == BREADCRUMB_IDX {
+                            x >= cx_c - r && x <= cx_c + r && y >= cy_c - r + 45.0 + MENUBAR_H && y <= cy_c - r + 45.0 + MENUBAR_H + BREADCRUMB_H
+                        } else {
+                            false
+                        }
+                    } else {
+                        state.widgets[i].hit_test(x, y)
+                    }
+                };
+
+                let in_circle_network_pane = if self.circular_network_pane {
+                    let dx = self.cursor_x - self.network_circle_x;
+                    let dy = self.cursor_y - self.network_circle_y;
+                    let r = self.network_circle_radius;
+                    dx * dx + dy * dy <= r * r && self.cursor_y >= self.network_circle_y - r + 45.0 + MENUBAR_H + BREADCRUMB_H
+                } else {
+                    in_network_pane
+                };
+
                 match btn_state {
                     ElementState::Pressed => {
-                        eprintln!("[DEBUG] Left click at cursor logical coords: ({:.2}, {:.2})", self.cursor_x, self.cursor_y);
+                        println!("DEBUG: Mouse Pressed button={:?} position=({}, {})", button, self.cursor_x, self.cursor_y);
+                        let _ = std::io::Write::flush(&mut std::io::stdout());
+                        if *button == MouseButton::Left && self.circular_network_pane {
+                            let dx = self.cursor_x - self.network_circle_x;
+                            let dy = self.cursor_y - self.network_circle_y;
+                            let dist = (dx * dx + dy * dy).sqrt();
+                            let on_border = dist >= self.network_circle_radius - 12.0 && dist <= self.network_circle_radius;
+                            let hit_menubar = self.widgets[LEFT_MENUBAR_IDX].hit_test(self.cursor_x, self.cursor_y);
+                            let mut clicked_menu_item = false;
+                            if hit_menubar {
+                                clicked_menu_item = self.widgets[LEFT_MENUBAR_IDX].mouse_input(*button, *btn_state, self.cursor_x, self.cursor_y);
+                            }
+                            if on_border || (hit_menubar && !clicked_menu_item) {
+                                self.is_dragging_network_circle = true;
+                                self.circle_drag_ox = self.cursor_x - self.network_circle_x;
+                                self.circle_drag_oy = self.cursor_y - self.network_circle_y;
+                                self.focused_pane = LEFT_MENUBAR_IDX;
+                                if let Some(old) = self.focused_widget {
+                                    self.widgets[old].unfocus();
+                                    self.focused_widget = None;
+                                }
+                                self.widgets[PARAM_IDX].unfocus();
+                                return true;
+                            } else if clicked_menu_item {
+                                self.focused_pane = LEFT_MENUBAR_IDX;
+                                if let Some(old) = self.focused_widget {
+                                    if old != LEFT_MENUBAR_IDX {
+                                        self.widgets[old].unfocus();
+                                    }
+                                }
+                                self.widgets[PARAM_IDX].unfocus();
+                                self.widgets[LEFT_MENUBAR_IDX].focus();
+                                self.focused_widget = Some(LEFT_MENUBAR_IDX);
+                                if !self.widgets[LEFT_MENUBAR_IDX].is_menu_open() {
+                                    self.widgets[LEFT_MENUBAR_IDX].unfocus();
+                                    self.focused_widget = None;
+                                }
+                                return true;
+                            }
+                        }
+
+                        if *button == MouseButton::Right {
+                            if !dialog_open && in_circle_network_pane {
+                                let col = ((self.cursor_x - self.pan_x) / (self.grid_size_x + self.skipped_col_w)).floor() as i32;
+                                let row = ((self.cursor_y - node_area_y - self.pan_y) / (self.grid_size_y + self.skipped_row_h)).floor() as i32;
+                                self.grid_cursor_col = col;
+                                self.grid_cursor_row = row;
+                                self.open_node_palette();
+                                return true;
+                            }
+                            return false;
+                        }
                         let mut click_target = None;
                         for i in 0..self.widgets.len() {
-                            if self.widgets[i].is_menu_open() && self.widgets[i].hit_test(self.cursor_x, self.cursor_y) {
+                            if self.widgets[i].is_menu_open() && hits_widget(self, i, self.cursor_x, self.cursor_y) {
                                 click_target = Some(i);
                                 break;
                             }
                         }
                         if click_target.is_none() {
                             click_target = (0..self.widgets.len()).rev()
-                                .find(|&i| self.widgets[i].hit_test(self.cursor_x, self.cursor_y));
-                        }
-                        eprintln!("[DEBUG] Selected click target: {:?}", click_target);
-                        if let Some(i) = click_target {
-                            let (rx, ry, rw, rh) = self.widgets[i].rect();
-                            eprintln!("[DEBUG] Target rect: ({:.2}, {:.2}, {:.2}, {:.2}), is_menu_bar: {}, is_menu_open: {}", rx, ry, rw, rh, self.widgets[i].is_menu_bar(), self.widgets[i].is_menu_open());
+                                .find(|&i| hits_widget(self, i, self.cursor_x, self.cursor_y));
                         }
 
                         // Determine new focused pane
                         let mut new_pane = None;
                         if let Some(i) = click_target {
-                            if i == LEFT_MENUBAR_IDX || i == CONTENT_IDX || i == CANVAS_IDX || (i >= NODE_SLOT_START && i < NODE_SLOT_START + NODE_SLOT_COUNT) || i == BREADCRUMB_IDX {
+                            if i == LEFT_MENUBAR_IDX || i == CONTENT_IDX || i == CANVAS_IDX || i == BREADCRUMB_IDX {
                                 new_pane = Some(LEFT_MENUBAR_IDX);
                             } else if i == RIGHT_MENUBAR_IDX || i == VIEWPORT_IDX {
                                 new_pane = Some(RIGHT_MENUBAR_IDX);
@@ -3726,15 +4281,27 @@ fn geometry_to_spreadsheet_data(geom: &Geometry) -> (Vec<String>, Vec<Vec<String
                                 new_pane = Some(SPREADSHEET_MENUBAR_IDX);
                             }
                         } else {
-                            if self.cursor_x < self.splitter1_x {
-                                new_pane = Some(LEFT_MENUBAR_IDX);
-                            } else if self.cursor_x > self.splitter2_x + SPLITTER_W {
-                                new_pane = Some(PARAM_MENUBAR_IDX);
+                            if self.circular_network_pane {
+                                if self.cursor_x > self.splitter2_x + SPLITTER_W {
+                                    new_pane = Some(PARAM_MENUBAR_IDX);
+                                } else {
+                                    if self.show_spreadsheet && self.cursor_y >= self.positions[SPREADSHEET_MENUBAR_IDX].1 {
+                                        new_pane = Some(SPREADSHEET_MENUBAR_IDX);
+                                    } else {
+                                        new_pane = Some(RIGHT_MENUBAR_IDX);
+                                    }
+                                }
                             } else {
-                                if self.show_spreadsheet && self.cursor_y >= self.positions[SPREADSHEET_MENUBAR_IDX].1 {
-                                    new_pane = Some(SPREADSHEET_MENUBAR_IDX);
+                                if self.cursor_x < self.splitter1_x {
+                                    new_pane = Some(LEFT_MENUBAR_IDX);
+                                } else if self.cursor_x > self.splitter2_x + SPLITTER_W {
+                                    new_pane = Some(PARAM_MENUBAR_IDX);
                                 } else {
-                                    new_pane = Some(RIGHT_MENUBAR_IDX);
+                                    if self.show_spreadsheet && self.cursor_y >= self.positions[SPREADSHEET_MENUBAR_IDX].1 {
+                                        new_pane = Some(SPREADSHEET_MENUBAR_IDX);
+                                    } else {
+                                        new_pane = Some(RIGHT_MENUBAR_IDX);
+                                    }
                                 }
                             }
                         }
@@ -3751,7 +4318,10 @@ fn geometry_to_spreadsheet_data(geom: &Geometry) -> (Vec<String>, Vec<Vec<String
                                 self.focused_widget = None;
                             }
                         }
-                        if click_target.is_none() && !dialog_open && in_network_pane {
+                        if click_target != Some(PARAM_IDX) {
+                            self.widgets[PARAM_IDX].unfocus();
+                        }
+                        if click_target.is_none() && !dialog_open && in_circle_network_pane {
                             let col = ((self.cursor_x - self.pan_x) / (self.grid_size_x + self.skipped_col_w)).floor() as i32;
                             let row = ((self.cursor_y - node_area_y - self.pan_y) / (self.grid_size_y + self.skipped_row_h)).floor() as i32;
                             self.grid_cursor_col = col;
@@ -3774,51 +4344,66 @@ fn geometry_to_spreadsheet_data(geom: &Geometry) -> (Vec<String>, Vec<Vec<String
                                     self.focused_widget = None;
                                 }
                             }
-                            if self.node_slots.contains(&i) {
-                                if let Some(slot_idx) = self.node_slots.iter().position(|&x| x == i) {
-                                    let off = self.left_offsets[slot_idx];
-                                    self.grid_cursor_col = off.0 as i32;
-                                    self.grid_cursor_row = off.1 as i32;
+                            if i == CONTENT_IDX {
+                                if let Some(slot_idx) = self.widgets[CONTENT_IDX].selected_node() {
+                                    let dir = self.current_dir();
+                                    if slot_idx < dir.children.len() {
+                                        let pos = dir.children[slot_idx].position;
+                                        self.grid_cursor_col = pos.0 as i32;
+                                        self.grid_cursor_row = pos.1 as i32;
+                                    }
+                                } else {
+                                    let col = ((self.cursor_x - self.pan_x) / (self.grid_size_x + self.skipped_col_w)).floor() as i32;
+                                    let row = ((self.cursor_y - node_area_y - self.pan_y) / (self.grid_size_y + self.skipped_row_h)).floor() as i32;
+                                    self.grid_cursor_col = col;
+                                    self.grid_cursor_row = row;
+                                    changed = true;
                                 }
-                            }
-                            // Double-click detection for node directory entry
-                            if self.node_slots.contains(&i) {
-                                if let Some((t, prev)) = self.last_click {
-                                    if prev == i && t.elapsed() < std::time::Duration::from_millis(500) {
-                                        let dir_idx = self.node_slots.iter().position(|&x| x == i).unwrap();
-                                        let dir = self.current_dir();
-                                        if dir_idx < dir.children.len() && (dir.children[dir_idx].node_type == "node" || dir.children[dir_idx].node_type == "opencl" || !dir.children[dir_idx].children.is_empty()) {
-                                            self.current_path.push(dir_idx);
-                                            self.on_path_changed();
-                                            changed = true;
-                                        }
+                                if let Some(dir_idx) = self.widgets[CONTENT_IDX].double_clicked_node() {
+                                    self.widgets[CONTENT_IDX].clear_double_clicked_node();
+                                    let dir = self.current_dir();
+                                    if dir_idx < dir.children.len() && (dir.children[dir_idx].node_type == "node" || dir.children[dir_idx].node_type == "opencl" || !dir.children[dir_idx].children.is_empty()) {
+                                        self.current_path.push(dir_idx);
+                                        self.on_path_changed();
+                                        changed = true;
                                     }
                                 }
-                                self.last_click = Some((Instant::now(), i));
-                            } else {
-                                self.last_click = None;
                             }
                         }
                         self.sync_pane_focus();
                     }
                     ElementState::Released => {
+                        if self.is_dragging_network_circle {
+                            self.is_dragging_network_circle = false;
+                            self.sync_layout();
+                            self.read_panel_offsets();
+                            self.upload_vertices();
+                            return true;
+                        }
                         if self.drag_widget.is_some() {
                             let idx = self.drag_widget.unwrap();
-                            if self.node_slots.contains(&idx) {
-                                if let Some(slot_idx) = self.node_slots.iter().position(|&x| x == idx) {
-                                    if slot_idx < self.current_dir().children.len() {
-                                        let pos = self.current_dir().children[slot_idx].position;
-                                        let (nx, ny) = self.find_empty_cell(pos.0, pos.1, Some(slot_idx));
-                                        self.current_dir_mut().children[slot_idx].position = (nx, ny);
-                                        self.left_offsets[slot_idx] = (nx, ny);
-                                        self.rebuild_positions();
-                                        self.apply_layout();
-                                        self.update_panel_bounds();
-                                        self.upload_vertices();
+                            if idx == CONTENT_IDX {
+                                self.widgets[idx].drag_end();
+                                let updated_nodes = self.widgets[CONTENT_IDX].get_nodes();
+                                let dir = self.current_dir_mut();
+                                for (i, node) in updated_nodes.iter().enumerate() {
+                                    if let Some(child) = dir.children.get_mut(i) {
+                                        child.position = node.position;
                                     }
                                 }
+                                if let Some(sel_idx) = self.widgets[CONTENT_IDX].selected_node() {
+                                    if let Some(node) = updated_nodes.get(sel_idx) {
+                                        self.grid_cursor_col = node.position.0 as i32;
+                                        self.grid_cursor_row = node.position.1 as i32;
+                                    }
+                                }
+                                self.rebuild_positions();
+                                self.apply_layout();
+                                self.update_panel_bounds();
+                                self.upload_vertices();
+                            } else {
+                                self.widgets[idx].drag_end();
                             }
-                            self.widgets[idx].drag_end();
                             self.drag_widget = None;
                             changed = true;
                         }
@@ -3830,15 +4415,8 @@ fn geometry_to_spreadsheet_data(geom: &Geometry) -> (Vec<String>, Vec<Vec<String
                     }
                 }
 
-                let active_nodes = self.current_dir().children.len().min(self.node_slots.len());
-                let mut visibility_updates = Vec::new();
-                for (i, &slot) in self.node_slots.iter().enumerate().take(active_nodes) {
-                    if self.widgets[slot].take_geom_toggle() {
-                        let visible = self.widgets[slot].geom_visible();
-                        visibility_updates.push((i, visible));
-                    }
-                }
-                for (i, visible) in visibility_updates {
+
+                if let Some((i, visible)) = self.widgets[CONTENT_IDX].take_node_geom_toggle() {
                     self.current_dir_mut().children[i].geometry_visible = visible;
                     self.rebuild_scene_geometry();
                     changed = true;
@@ -3867,16 +4445,14 @@ fn geometry_to_spreadsheet_data(geom: &Geometry) -> (Vec<String>, Vec<Vec<String
                 }
 
                 if self.widgets[PARAM_IDX].keyboard_input(event) {
-                    if let Some(focused) = self.focused_widget {
-                        if self.node_slots.contains(&focused) {
-                            if let Some(slot_idx) = self.node_slots.iter().position(|&x| x == focused) {
-                                let updated_params = self.widgets[PARAM_IDX].node_params();
-                                let dir = self.current_dir_mut();
-                                if let Some(child) = dir.children.get_mut(slot_idx) {
-                                    for (u_name, u_val, _) in &updated_params {
-                                        if let Some(p) = child.params.iter_mut().find(|p| p.name == *u_name) {
-                                            p.default = u_val.clone();
-                                        }
+                    if self.focused_widget == Some(CONTENT_IDX) {
+                        if let Some(slot_idx) = self.widgets[CONTENT_IDX].selected_node() {
+                            let updated_params = self.widgets[PARAM_IDX].node_params();
+                            let dir = self.current_dir_mut();
+                            if let Some(child) = dir.children.get_mut(slot_idx) {
+                                for (u_name, u_val, _) in &updated_params {
+                                    if let Some(p) = child.params.iter_mut().find(|p| p.name == *u_name) {
+                                        p.default = u_val.clone();
                                     }
                                 }
                             }
@@ -3913,16 +4489,16 @@ fn geometry_to_spreadsheet_data(geom: &Geometry) -> (Vec<String>, Vec<Vec<String
                             }
                             Key::Character(s) => {
                                 match s.as_str() {
-                                    "k" | "K" if is_plain_key || is_alt_key => {
+                                    "k" | "K" if is_plain_key || (is_alt_key && self.focused_pane == LEFT_MENUBAR_IDX) => {
                                         delta = Some((0, -1));
                                     }
-                                    "j" | "J" if is_plain_key || is_alt_key => {
+                                    "j" | "J" if is_plain_key || (is_alt_key && self.focused_pane == LEFT_MENUBAR_IDX) => {
                                         delta = Some((0, 1));
                                     }
-                                    "h" | "H" if is_plain_key || is_alt_key => {
+                                    "h" | "H" if is_plain_key || (is_alt_key && self.focused_pane == LEFT_MENUBAR_IDX) => {
                                         delta = Some((-1, 0));
                                     }
-                                    "l" | "L" if is_plain_key || is_alt_key => {
+                                    "l" | "L" if is_plain_key || (is_alt_key && self.focused_pane == LEFT_MENUBAR_IDX) => {
                                         delta = Some((1, 0));
                                     }
                                     _ => {}
@@ -3938,7 +4514,7 @@ fn geometry_to_spreadsheet_data(geom: &Geometry) -> (Vec<String>, Vec<Vec<String
                             self.scroll_accum_x = 0.0;
                             self.scroll_accum_y = 0.0;
                             if is_alt_key {
-                                let active_nodes = self.current_dir().children.len().min(self.node_slots.len());
+                                let active_nodes = self.current_dir().children.len();
                                 let node_idx_at_cursor = self.current_dir().children.iter().take(active_nodes).position(|child| {
                                     child.position.0 as i32 == self.grid_cursor_col && child.position.1 as i32 == self.grid_cursor_row
                                 });
@@ -3946,7 +4522,6 @@ fn geometry_to_spreadsheet_data(geom: &Geometry) -> (Vec<String>, Vec<Vec<String
                                     let new_x = self.current_dir().children[idx].position.0 + dc as f32;
                                     let new_y = self.current_dir().children[idx].position.1 + dr as f32;
                                     self.current_dir_mut().children[idx].position = (new_x, new_y);
-                                    self.left_offsets[idx] = (new_x, new_y);
                                     self.sync_nodes();
                                     self.sync_layout();
                                     self.upload_vertices();
@@ -3954,19 +4529,20 @@ fn geometry_to_spreadsheet_data(geom: &Geometry) -> (Vec<String>, Vec<Vec<String
                             }
                             self.grid_cursor_col += dc;
                             self.grid_cursor_row += dr;
+                            self.sync_cursor_and_selection();
                             changed = true;
                         } else {
                             if let Key::Character(s) = &event.logical_key {
                                 if is_plain_key {
                                     match s.as_str() {
                                         "e" | "E" => {
-                                            if let Some(idx) = self.focused_widget {
-                                                if let Some(slot_idx) = self.node_slots.iter().position(|&x| x == idx) {
-                                                    let active_nodes = self.current_dir().children.len().min(self.node_slots.len());
-                                                    if slot_idx < active_nodes {
-                                                        let visible = !self.widgets[idx].geom_visible();
-                                                        self.widgets[idx].set_geom_visible(visible);
+                                            if self.focused_widget == Some(CONTENT_IDX) {
+                                                if let Some(slot_idx) = self.widgets[CONTENT_IDX].selected_node() {
+                                                    let dir = self.current_dir();
+                                                    if slot_idx < dir.children.len() {
+                                                        let visible = !dir.children[slot_idx].geometry_visible;
                                                         self.current_dir_mut().children[slot_idx].geometry_visible = visible;
+                                                        self.sync_nodes();
                                                         self.rebuild_scene_geometry();
                                                         changed = true;
                                                     }
@@ -3998,14 +4574,14 @@ fn geometry_to_spreadsheet_data(geom: &Geometry) -> (Vec<String>, Vec<Vec<String
                                         }
                                         "i" | "I" => {
                                             if self.focused_pane == LEFT_MENUBAR_IDX {
-                                                if let Some(focused) = self.focused_widget {
-                                                    if let Some(slot_idx) = self.node_slots.iter().position(|&x| x == focused) {
+                                                if self.focused_widget == Some(CONTENT_IDX) {
+                                                    if let Some(slot_idx) = self.widgets[CONTENT_IDX].selected_node() {
                                                         let dir = self.current_dir();
                                                          if slot_idx < dir.children.len() && (dir.children[slot_idx].node_type == "node" || dir.children[slot_idx].node_type == "opencl" || !dir.children[slot_idx].children.is_empty()) {
                                                              self.current_path.push(slot_idx);
-                                                            self.on_path_changed();
-                                                            changed = true;
-                                                        }
+                                                             self.on_path_changed();
+                                                             changed = true;
+                                                         }
                                                     }
                                                 }
                                             }
@@ -4020,7 +4596,7 @@ fn geometry_to_spreadsheet_data(geom: &Geometry) -> (Vec<String>, Vec<Vec<String
                                         }
                                         "f" | "F" => {
                                             if self.focused_pane == LEFT_MENUBAR_IDX {
-                                                let active_nodes = self.current_dir().children.len().min(self.node_slots.len());
+                                                let active_nodes = self.current_dir().children.len();
                                                 if active_nodes == 0 {
                                                     self.grid_size_x = 80.0;
                                                     self.grid_size_y = 40.0;
@@ -4040,7 +4616,7 @@ fn geometry_to_spreadsheet_data(geom: &Geometry) -> (Vec<String>, Vec<Vec<String
                                                     let mut b_ymax = f32::MIN;
 
                                                     for slot_idx in 0..active_nodes {
-                                                        let (col, row) = self.left_offsets[slot_idx];
+                                                        let (col, row) = self.current_dir().children[slot_idx].position;
                                                         let x_min = col * (base_gx + base_col_w);
                                                         let x_max = x_min + base_gx;
                                                         let y_min = row * (base_gy + base_row_h);
@@ -4080,7 +4656,7 @@ fn geometry_to_spreadsheet_data(geom: &Geometry) -> (Vec<String>, Vec<Vec<String
                                                     let mut actual_ymax = f32::MIN;
 
                                                     for slot_idx in 0..active_nodes {
-                                                        let (col, row) = self.left_offsets[slot_idx];
+                                                        let (col, row) = self.current_dir().children[slot_idx].position;
                                                         let x_min = col * (self.grid_size_x + self.skipped_col_w);
                                                         let x_max = x_min + self.grid_size_x;
                                                         let y_min = row * (self.grid_size_y + self.skipped_row_h);
@@ -4145,6 +4721,10 @@ fn geometry_to_spreadsheet_data(geom: &Geometry) -> (Vec<String>, Vec<Vec<String
         let dt = now.duration_since(self.last_frame).as_secs_f32().min(0.1);
         self.last_frame = now;
 
+        if now.duration_since(self.last_config_read).as_secs_f32() > 2.0 {
+            self.update_inertial_settings();
+        }
+
         // Panning velocity tracking
         if self.is_panning {
             if dt > 1e-5 {
@@ -4206,26 +4786,30 @@ fn geometry_to_spreadsheet_data(geom: &Geometry) -> (Vec<String>, Vec<Vec<String
 
         // Panning kinetic slide
         if !self.is_panning && !self.is_scrolling_trackpad && (self.pan_velocity_x.abs() > 0.01 || self.pan_velocity_y.abs() > 0.01) {
-            self.pan_x += self.pan_velocity_x * dt;
-            self.pan_y += self.pan_velocity_y * dt;
-
-            // Apply friction decay
-            let friction = 5.0_f32;
-            let decay = (-friction * dt).exp();
-            self.pan_velocity_x *= decay;
-            self.pan_velocity_y *= decay;
-
-            // If velocity magnitude is below 10.0 px/sec, stop completely.
-            if self.pan_velocity_x.hypot(self.pan_velocity_y) < 10.0 {
+            if !self.inertial_scroll_enabled {
                 self.pan_velocity_x = 0.0;
                 self.pan_velocity_y = 0.0;
-            }
+            } else {
+                self.pan_x += self.pan_velocity_x * dt;
+                self.pan_y += self.pan_velocity_y * dt;
 
-            self.sync_grid_settings();
-            self.rebuild_positions();
-            self.apply_layout();
-            self.update_panel_bounds();
-            tick_changed = true;
+                // Apply dynamic friction decay
+                let decay = self.inertial_scroll_friction.powf(dt * 60.0);
+                self.pan_velocity_x *= decay;
+                self.pan_velocity_y *= decay;
+
+                // If velocity magnitude is below 10.0 px/sec, stop completely.
+                if self.pan_velocity_x.hypot(self.pan_velocity_y) < 10.0 {
+                    self.pan_velocity_x = 0.0;
+                    self.pan_velocity_y = 0.0;
+                }
+
+                self.sync_grid_settings();
+                self.rebuild_positions();
+                self.apply_layout();
+                self.update_panel_bounds();
+                tick_changed = true;
+            }
         }
 
         // Viewport rotation kinetic slide
@@ -4267,7 +4851,7 @@ fn geometry_to_spreadsheet_data(geom: &Geometry) -> (Vec<String>, Vec<Vec<String
 
         let mut panned = false;
         if let Some(idx) = self.drag_widget {
-            if self.node_slots.contains(&idx) {
+            if idx == CONTENT_IDX {
                 let node_area_y = HEADER_H + MENUBAR_H + BREADCRUMB_H;
                 let margin = 30.0_f32;
                 let pan_speed = 5.0_f32;
@@ -4678,12 +5262,14 @@ impl PointerHandler for AppState {
         for event in events {
             if let Some(st) = &mut self.state {
                 let (cx, cy) = clear_ui::wayland::scale_pointer_pos(event.position, st.scale);
+                st.cursor_x = event.position.0 as f32;
+                st.cursor_y = event.position.1 as f32;
                 match &event.kind {
                     PointerEventKind::Motion { .. } => {
                         let ev = WindowEvent::CursorMoved {
                             position: LocalPosition {
-                                x: cx as f64,
-                                y: cy as f64,
+                                x: event.position.0,
+                                y: event.position.1,
                             },
                         };
                         self.process_event(ev);
@@ -4695,6 +5281,7 @@ impl PointerHandler for AppState {
                             274 => clear_ui::widget::MouseButton::Middle,
                             _ => continue,
                         };
+                        eprintln!("DEBUG MOUSE PRESS: button={:?}, pos={:?}, local=({}, {})", btn, event.position, cx, cy);
                         let ev = WindowEvent::MouseInput {
                             state: clear_ui::widget::ElementState::Pressed,
                             button: btn,
@@ -4708,6 +5295,7 @@ impl PointerHandler for AppState {
                             274 => clear_ui::widget::MouseButton::Middle,
                             _ => continue,
                         };
+                        eprintln!("DEBUG MOUSE RELEASE: button={:?}, pos={:?}, local=({}, {})", btn, event.position, cx, cy);
                         let ev = WindowEvent::MouseInput {
                             state: clear_ui::widget::ElementState::Released,
                             button: btn,
@@ -4754,7 +5342,13 @@ impl KeyboardHandler for AppState {
         _keyboard: &wl_keyboard::WlKeyboard,
         _surface: &wl_surface::WlSurface,
         _serial: u32,
-    ) {}
+    ) {
+        self.pressed_key = None;
+        if let Some(st) = &mut self.state {
+            st.modifiers = ModifiersState::default();
+        }
+    }
+
 
     fn press_key(
         &mut self,
@@ -4784,9 +5378,16 @@ impl KeyboardHandler for AppState {
         _qh: &QueueHandle<Self>,
         _keyboard: &wl_keyboard::WlKeyboard,
         _serial: u32,
-        _modifiers: smithay_client_toolkit::seat::keyboard::Modifiers,
+        modifiers: smithay_client_toolkit::seat::keyboard::Modifiers,
         _layout: u32,
-    ) {}
+    ) {
+        if let Some(st) = &mut self.state {
+            st.modifiers.ctrl = modifiers.ctrl;
+            st.modifiers.alt = modifiers.alt;
+            st.modifiers.shift = modifiers.shift;
+            st.modifiers.logo = modifiers.logo;
+        }
+    }
 }
 
 impl WindowHandler for AppState {
@@ -4878,17 +5479,26 @@ impl AppState {
             xkeysym::Keysym::g | xkeysym::Keysym::G => Key::Character("g".into()),
             xkeysym::Keysym::e | xkeysym::Keysym::E => Key::Character("e".into()),
             xkeysym::Keysym::a | xkeysym::Keysym::A => Key::Character("a".into()),
+            xkeysym::Keysym::d | xkeysym::Keysym::D => Key::Character("d".into()),
             xkeysym::Keysym::f | xkeysym::Keysym::F => Key::Character("f".into()),
+            xkeysym::Keysym::h | xkeysym::Keysym::H => Key::Character("h".into()),
+            xkeysym::Keysym::j | xkeysym::Keysym::J => Key::Character("j".into()),
+            xkeysym::Keysym::k | xkeysym::Keysym::K => Key::Character("k".into()),
+            xkeysym::Keysym::l | xkeysym::Keysym::L => Key::Character("l".into()),
             xkeysym::Keysym::grave => Key::Character("`".into()),
             _ => {
                 if let Some(ref text) = event.utf8 {
                     Key::Character(text.clone())
+                } else if let Some(ch) = event.keysym.key_char() {
+                    Key::Character(ch.to_string())
                 } else {
                     return;
                 }
             }
         };
 
+        eprintln!("DEBUG KEY: keysym={:?}, state={:?}, logical_key={:?}", event.keysym, state, logical_key);
+
         if let Some(st) = &mut self.state {
             match event.keysym {
                 xkeysym::Keysym::Control_L | xkeysym::Keysym::Control_R => {
@@ -5012,6 +5622,18 @@ impl AppState {
                         }
                         _ => {}
                     }
+                } else if menu_idx == 2 { // View
+                    match item_idx {
+                        2 => {
+                            state.circular_network_pane = !state.circular_network_pane;
+                            state.widgets[LEFT_MENUBAR_IDX].set_item_checked(2, 2, state.circular_network_pane);
+                            state.rebuild_positions();
+                            state.apply_layout();
+                            state.sync_grid_settings();
+                            changed = true;
+                        }
+                        _ => {}
+                    }
                 }
             }
 
@@ -5054,11 +5676,7 @@ impl AppState {
                 match id {
                     0 => {
                         state.grid_snap_enabled = val;
-                        for &i in &state.node_slots {
-                            let gx = if val { state.grid_size_x + state.skipped_col_w } else { 0.0 };
-                            let gy = if val { state.grid_size_y + state.skipped_row_h } else { 0.0 };
-                            state.widgets[i].set_grid_snap(gx, gy);
-                        }
+                        state.widgets[CONTENT_IDX].set_grid_snap_enabled(val);
                     }
                     1 => {
                         state.network_grid_visible = val;
@@ -5107,12 +5725,20 @@ impl AppState {
                         state.camera_pivot_size = val;
                         state.update_pivot_geometry();
                     }
+                    10 => {
+                        state.node_color[0] = val;
+                        colors::set_node_color([state.node_color[0], state.node_color[1], state.node_color[2], 1.0]);
+                    }
+                    11 => {
+                        state.node_color[1] = val;
+                        colors::set_node_color([state.node_color[0], state.node_color[1], state.node_color[2], 1.0]);
+                    }
+                    12 => {
+                        state.node_color[2] = val;
+                        colors::set_node_color([state.node_color[0], state.node_color[1], state.node_color[2], 1.0]);
+                    }
                     _ => {}
                 }
-                for &i in &state.node_slots {
-                    let (x, y, _, _) = state.widgets[i].rect();
-                    state.widgets[i].set_rect(x, y, state.grid_size_x, state.grid_size_y);
-                }
                 state.sync_grid_settings();
                 changed = true;
                 settings_changed = true;
@@ -5128,24 +5754,22 @@ impl AppState {
                 state.sync_cursor_and_selection();
 
                 if state.drag_widget == Some(PARAM_IDX) && state.widgets[PARAM_IDX].is_dragging() {
-                    if let Some(focused) = state.focused_widget {
-                        if state.node_slots.contains(&focused) {
-                            if let Some(slot_idx) = state.node_slots.iter().position(|&x| x == focused) {
-                                let updated_params = state.widgets[PARAM_IDX].node_params();
-                                let dir = state.current_dir_mut();
-                                if let Some(child) = dir.children.get_mut(slot_idx) {
-                                    let mut param_changed = false;
-                                    for (u_name, u_val, _type) in &updated_params {
-                                        if let Some(p) = child.params.iter_mut().find(|p| p.name == *u_name) {
-                                            if p.default != *u_val {
-                                                p.default = u_val.clone();
-                                                param_changed = true;
-                                            }
+                    if state.focused_widget == Some(CONTENT_IDX) {
+                        if let Some(slot_idx) = state.widgets[CONTENT_IDX].selected_node() {
+                            let updated_params = state.widgets[PARAM_IDX].node_params();
+                            let dir = state.current_dir_mut();
+                            if let Some(child) = dir.children.get_mut(slot_idx) {
+                                let mut param_changed = false;
+                                for (u_name, u_val, _type) in &updated_params {
+                                    if let Some(p) = child.params.iter_mut().find(|p| p.name == *u_name) {
+                                        if p.default != *u_val {
+                                            p.default = u_val.clone();
+                                            param_changed = true;
                                         }
                                     }
-                                    if param_changed {
-                                        state.rebuild_scene_geometry();
-                                    }
+                                }
+                                if param_changed {
+                                    state.rebuild_scene_geometry();
                                 }
                             }
                         }
@@ -5155,9 +5779,16 @@ impl AppState {
                 state.sync_nodes();
 
                 // Sync Parameters pane with selected node
-                let params = state.focused_widget.and_then(|f| {
-                    if state.node_slots.contains(&f) { Some(f) } else { None }
-                }).map(|f| state.widgets[f].node_params()).unwrap_or_default();
+                let params = if state.focused_widget == Some(CONTENT_IDX) {
+                    state.widgets[CONTENT_IDX].selected_node().and_then(|sel_idx| {
+                        let dir = state.current_dir();
+                        if sel_idx < dir.children.len() {
+                            Some(param_display(&dir.children[sel_idx].params))
+                        } else { None }
+                    }).unwrap_or_default()
+                } else {
+                    vec![]
+                };
                 state.widgets[PARAM_IDX].set_display_params(&params);
 
                 state.upload_vertices();
@@ -5261,12 +5892,11 @@ impl AppState {
                             }
                         }
                         HttpAction::ToggleGeometry { slot } => {
-                            let active_nodes = state.current_dir().children.len().min(state.node_slots.len());
+                            let active_nodes = state.current_dir().children.len();
                             if slot < active_nodes {
-                                let idx = state.node_slots[slot];
-                                let visible = !state.widgets[idx].geom_visible();
-                                state.widgets[idx].set_geom_visible(visible);
+                                let visible = !state.current_dir().children[slot].geometry_visible;
                                 state.current_dir_mut().children[slot].geometry_visible = visible;
+                                state.sync_nodes();
                                 state.rebuild_scene_geometry();
                                 needs_redraw = true;
                                 Ok(format!("Geometry visible: {}", visible))
@@ -5275,33 +5905,27 @@ impl AppState {
                             }
                         }
                         HttpAction::AddNode { template_name, name, x, y } => {
-                            let child_idx = state.current_dir().children.len();
-                            if child_idx >= state.node_slots.len() {
-                                Err("Max node slots reached".to_string())
-                            } else {
-                                let template_idx = state.node_templates.iter().position(|t| {
-                                    t.label.to_lowercase() == template_name.to_lowercase()
-                                        || t.node.name.to_lowercase() == template_name.to_lowercase()
-                                });
-                                if let Some(idx) = template_idx {
-                                    let mut node = state.node_templates[idx].node.clone();
-                                    let (nx, ny) = state.find_empty_cell(x, y, None);
-                                    node.position = (nx, ny);
-                                    if let Some(n) = name {
-                                        node.name = n;
-                                    }
-                                    state.current_dir_mut().children.push(node);
-                                    state.left_offsets[child_idx] = (nx, ny);
-                                    state.sync_nodes();
-                                    state.rebuild_positions();
-                                    state.apply_layout();
-                                    state.update_panel_bounds();
-                                    state.upload_vertices();
-                                    needs_redraw = true;
-                                    Ok("Node added".to_string())
-                                } else {
-                                    Err(format!("Template '{}' not found", template_name))
+                            let template_idx = state.node_templates.iter().position(|t| {
+                                t.label.to_lowercase() == template_name.to_lowercase()
+                                    || t.node.name.to_lowercase() == template_name.to_lowercase()
+                            });
+                            if let Some(idx) = template_idx {
+                                let mut node = state.node_templates[idx].node.clone();
+                                let (nx, ny) = state.find_empty_cell(x, y, None);
+                                node.position = (nx, ny);
+                                if let Some(n) = name {
+                                    node.name = n;
                                 }
+                                state.current_dir_mut().children.push(node);
+                                state.sync_nodes();
+                                state.rebuild_positions();
+                                state.apply_layout();
+                                state.update_panel_bounds();
+                                state.upload_vertices();
+                                needs_redraw = true;
+                                Ok("Node added".to_string())
+                            } else {
+                                Err(format!("Template '{}' not found", template_name))
                             }
                         }
                         HttpAction::DeleteNode { slot } => {
@@ -5309,10 +5933,13 @@ impl AppState {
                             if slot < len {
                                 state.current_dir_mut().children.remove(slot);
                                 if let Some(focused) = state.focused_widget {
-                                    if let Some(pos) = state.node_slots.iter().position(|&x| x == focused) {
-                                        if pos >= slot {
-                                            state.widgets[focused].unfocus();
-                                            state.focused_widget = None;
+                                    if focused == CONTENT_IDX {
+                                        if let Some(sel_idx) = state.widgets[CONTENT_IDX].selected_node() {
+                                            if sel_idx == slot {
+                                                state.widgets[CONTENT_IDX].set_selected_node(None);
+                                            } else if sel_idx > slot {
+                                                state.widgets[CONTENT_IDX].set_selected_node(Some(sel_idx - 1));
+                                            }
                                         }
                                     }
                                 }
@@ -5343,9 +5970,6 @@ impl AppState {
                             if slot < len {
                                 let (nx, ny) = state.find_empty_cell(x, y, Some(slot));
                                 state.current_dir_mut().children[slot].position = (nx, ny);
-                                if slot < state.left_offsets.len() {
-                                    state.left_offsets[slot] = (nx, ny);
-                                }
                                 state.sync_nodes();
                                 state.rebuild_positions();
                                 state.apply_layout();
@@ -5393,6 +6017,16 @@ impl AppState {
                                 Err("Slot out of bounds".to_string())
                             }
                         }
+                        HttpAction::ToggleCircularPane => {
+                            state.circular_network_pane = !state.circular_network_pane;
+                            state.widgets[LEFT_MENUBAR_IDX].set_item_checked(2, 2, state.circular_network_pane);
+                            state.rebuild_positions();
+                            state.apply_layout();
+                            state.sync_grid_settings();
+                            state.upload_vertices();
+                            needs_redraw = true;
+                            Ok(format!("Circular pane: {}", state.circular_network_pane))
+                        }
                     };
                     let _ = tx.send(res);
                 }
@@ -5670,6 +6304,22 @@ fn main() {
             }
         }
 
+fn create_memfd_with_data(name: &str, data: &[u8]) -> std::io::Result<std::os::unix::io::RawFd> {
+    use std::io::{Seek, Write};
+    use std::os::unix::io::FromRawFd;
+    use std::os::unix::io::IntoRawFd;
+
+    let c_name = std::ffi::CString::new(name).unwrap();
+    let fd = unsafe { libc::memfd_create(c_name.as_ptr(), libc::MFD_CLOEXEC) };
+    if fd < 0 {
+        return Err(std::io::Error::last_os_error());
+    }
+    let mut file = unsafe { std::fs::File::from_raw_fd(fd) };
+    file.write_all(data)?;
+    file.seek(std::io::SeekFrom::Start(0))?;
+    Ok(file.into_raw_fd())
+}
+
         if app.redraw {
             app.redraw = false;
             if let Some(state) = &mut app.state {
@@ -5679,7 +6329,11 @@ fn main() {
                 if let Some(ref inspector) = app.inspector {
                     if let Some(ref surface) = app.surface {
                         let json = clear_ui::widget::serialize_widgets(&state.widgets);
-                        inspector.update_state(surface, json);
+                        if let Ok(raw_fd) = create_memfd_with_data("clear_ui_state", json.as_bytes()) {
+                            use std::os::unix::io::{FromRawFd, AsFd};
+                            let file = unsafe { std::fs::File::from_raw_fd(raw_fd) };
+                            inspector.update_state(surface, file.as_fd(), json.len() as u32);
+                        }
                     }
                 }
             }
@@ -5908,5 +6562,27 @@ mod tests {
         assert_eq!(get_next_visible_pane(PARAM_MENUBAR_IDX, true, true), RIGHT_MENUBAR_IDX);
         assert_eq!(get_next_visible_pane(RIGHT_MENUBAR_IDX, true, true), LEFT_MENUBAR_IDX);
     }
+
+    #[test]
+    fn test_inertial_settings_fallback_and_ranges() {
+        let friction_raw = 90_u16;
+        let friction_f32 = (friction_raw as f32 / 100.0).clamp(0.5, 0.99);
+        assert!(friction_f32 >= 0.5 && friction_f32 <= 0.99);
+        
+        let friction_raw_low = 30_u16;
+        let friction_f32_low = (friction_raw_low as f32 / 100.0).clamp(0.5, 0.99);
+        assert_eq!(friction_f32_low, 0.5);
+    }
+
+    #[test]
+    fn test_decay_formula() {
+        let friction = 0.90_f32;
+        let dt_60 = 1.0 / 60.0;
+        let decay_60 = friction.powf(dt_60 * 60.0);
+        assert!((decay_60 - friction).abs() < 1e-5);
+
+        let decay_0 = friction.powf(0.0 * 60.0);
+        assert_eq!(decay_0, 1.0);
+    }
 }
 
diff --git a/src/shader.wgsl b/src/shader.wgsl
index 8493b48..47fc35b 100644
--- a/src/shader.wgsl
+++ b/src/shader.wgsl
@@ -1,20 +1,31 @@
 struct VertexOutput {
     @builtin(position) clip_position: vec4f,
     @location(0) color: vec4f,
+    @location(1) clip_circle: vec3f,
 }
 
 @vertex
 fn vs_main(
     @location(0) position: vec2f,
     @location(1) color: vec4f,
+    @location(2) clip_circle: vec3f,
 ) -> VertexOutput {
     var out: VertexOutput;
     out.clip_position = vec4f(position, 0.0, 1.0);
     out.color = color;
+    out.clip_circle = clip_circle;
     return out;
 }
 
 @fragment
 fn fs_main(in: VertexOutput) -> @location(0) vec4f {
+    if (in.clip_circle.z > 0.0) {
+        let dx = in.clip_position.x - in.clip_circle.x;
+        let dy = in.clip_position.y - in.clip_circle.y;
+        if (dx * dx + dy * dy > in.clip_circle.z * in.clip_circle.z) {
+            discard;
+        }
+    }
     return in.color;
 }
+