From a73ea90fac8d12b5bfe8b7e5a27d6951b798487d Mon Sep 17 00:00:00 2001
From: reze-dev <25588579+reze-dev@users.noreply.github.com>
Date: Sat, 26 Sep 2026 01:22:43 +0530
Subject: [PATCH 01/48] feat(radial): add RadialGeometry math and SVG path
library
---
.../desktop/surfaces/radial/RadialGeometry.js | 182 ++++++++++++++++++
1 file changed, 182 insertions(+)
create mode 100644 shell/desktop/surfaces/radial/RadialGeometry.js
diff --git a/shell/desktop/surfaces/radial/RadialGeometry.js b/shell/desktop/surfaces/radial/RadialGeometry.js
new file mode 100644
index 00000000..d32968df
--- /dev/null
+++ b/shell/desktop/surfaces/radial/RadialGeometry.js
@@ -0,0 +1,182 @@
+.pragma library
+
+// =============================================================================
+// RadialGeometry.js - Pure mathematical and geometric algorithms for ctOS
+// =============================================================================
+
+function degToRad(degrees) {
+ return (degrees * Math.PI) / 180.0;
+}
+
+function radToDeg(radians) {
+ return (radians * 180.0) / Math.PI;
+}
+
+// Normalizes any angle into [-180, 180] range
+function normalizeAngle(degrees) {
+ var a = degrees % 360;
+ if (a > 180) a -= 360;
+ if (a < -180) a += 360;
+ return a;
+}
+
+// Calculates shortest angular distance from 'fromAngle' to 'toAngle' in [-180, 180]
+function angleDifference(fromAngle, toAngle) {
+ return normalizeAngle(toAngle - fromAngle);
+}
+
+// Calculates Cartesian point on circle
+function pointOnCircle(cx, cy, radius, angleDeg) {
+ var rad = degToRad(angleDeg);
+ return {
+ x: cx + radius * Math.cos(rad),
+ y: cy + radius * Math.sin(rad)
+ };
+}
+
+// Calculates angle in degrees [-180, 180] from center (cx, cy) to target point (x, y)
+function angleFromCenter(cx, cy, x, y) {
+ var dx = x - cx;
+ var dy = y - cy;
+ var rad = Math.atan2(dy, dx);
+ return radToDeg(rad);
+}
+
+// Euclidean distance between two points
+function distance(x1, y1, x2, y2) {
+ var dx = x2 - x1;
+ var dy = y2 - y1;
+ return Math.sqrt(dx * dx + dy * dy);
+}
+
+// Generates SVG Path string for an annular sector (ring slice)
+// Supports innerRadius, outerRadius, startAngleDeg, endAngleDeg
+function createSectorSvgPath(cx, cy, innerRadius, outerRadius, startAngleDeg, endAngleDeg) {
+ var rInner = Math.max(0.1, innerRadius);
+ var rOuter = Math.max(rInner + 1.0, outerRadius);
+
+ var a1 = degToRad(startAngleDeg);
+ var a2 = degToRad(endAngleDeg);
+
+ var sweep = endAngleDeg - startAngleDeg;
+ while (sweep < 0) sweep += 360;
+ var largeArc = (sweep > 180) ? 1 : 0;
+
+ var pOut1 = { x: cx + rOuter * Math.cos(a1), y: cy + rOuter * Math.sin(a1) };
+ var pOut2 = { x: cx + rOuter * Math.cos(a2), y: cy + rOuter * Math.sin(a2) };
+ var pIn1 = { x: cx + rInner * Math.cos(a1), y: cy + rInner * Math.sin(a1) };
+ var pIn2 = { x: cx + rInner * Math.cos(a2), y: cy + rInner * Math.sin(a2) };
+
+ // Format numbers with 2 decimal places for clean SVG
+ function fmt(n) { return n.toFixed(2); }
+
+ return "M " + fmt(pIn1.x) + " " + fmt(pIn1.y) +
+ " L " + fmt(pOut1.x) + " " + fmt(pOut1.y) +
+ " A " + fmt(rOuter) + " " + fmt(rOuter) + " 0 " + largeArc + " 1 " + fmt(pOut2.x) + " " + fmt(pOut2.y) +
+ " L " + fmt(pIn2.x) + " " + fmt(pIn2.y) +
+ " A " + fmt(rInner) + " " + fmt(rInner) + " 0 " + largeArc + " 0 " + fmt(pIn1.x) + " " + fmt(pIn1.y) +
+ " Z";
+}
+
+// Returns the segment index [0, segmentCount - 1] that contains or is closest to target angle
+// Segments are centered starting from top (-90 deg), progressing clockwise
+function findClosestSegment(targetAngleDeg, segmentCount, currentFocusedIndex, hysteresisDegrees) {
+ if (segmentCount <= 0) return 0;
+ var segStep = 360.0 / segmentCount;
+ var hBias = (typeof hysteresisDegrees === "number") ? hysteresisDegrees : 3.0;
+
+ var bestIndex = 0;
+ var minDiff = 999.0;
+
+ for (var i = 0; i < segmentCount; ++i) {
+ // Base center angle for segment i:
+ // Segment 0 at -90 deg (12 o'clock)
+ var centerAngle = -90.0 + i * segStep;
+ var diff = Math.abs(angleDifference(targetAngleDeg, centerAngle));
+
+ // Apply hysteresis bias in favor of current selection
+ if (i === currentFocusedIndex) {
+ diff -= hBias;
+ }
+
+ if (diff < minDiff) {
+ minDiff = diff;
+ bestIndex = i;
+ }
+ }
+
+ return bestIndex;
+}
+
+// Finds closest node in a 2D node list with hysteresis to prevent cursor flutter
+function findClosestNode(cursorX, cursorY, nodes, currentNodeId, hysteresisRadius) {
+ if (!nodes || nodes.length === 0) return null;
+ var hBias = (typeof hysteresisRadius === "number") ? hysteresisRadius : 20.0;
+
+ var bestNode = null;
+ var minDist = 999999.0;
+
+ for (var i = 0; i < nodes.length; ++i) {
+ var node = nodes[i];
+ if (!node || typeof node.screenX !== "number" || typeof node.screenY !== "number") continue;
+
+ var d = distance(cursorX, cursorY, node.screenX, node.screenY);
+ if (node.id === currentNodeId) {
+ d -= hBias;
+ }
+
+ if (d < minDist) {
+ minDist = d;
+ bestNode = node;
+ }
+ }
+
+ return bestNode;
+}
+
+// Finds the best neighbor node in a given spatial direction (0=Up, 1=Right, 2=Down, 3=Left)
+function findSpatialNeighbor(currentNode, allNodes, direction) {
+ if (!currentNode || !allNodes || allNodes.length <= 1) return currentNode;
+
+ var cx = currentNode.screenX;
+ var cy = currentNode.screenY;
+ var bestNeighbor = null;
+ var bestScore = -999999.0;
+
+ for (var i = 0; i < allNodes.length; ++i) {
+ var n = allNodes[i];
+ if (n.id === currentNode.id) continue;
+
+ var dx = n.screenX - cx;
+ var dy = n.screenY - cy;
+ var d = Math.sqrt(dx * dx + dy * dy);
+ if (d < 1.0) continue;
+
+ var score = 0;
+ // Direction vectors:
+ // Up: dx=0, dy=-1
+ // Right: dx=1, dy=0
+ // Down: dx=0, dy=1
+ // Left: dx=-1, dy=0
+ if (direction === 0) { // Up
+ if (dy >= -5) continue; // must be above
+ score = (-dy * 2.0) - Math.abs(dx);
+ } else if (direction === 1) { // Right
+ if (dx <= 5) continue; // must be right
+ score = (dx * 2.0) - Math.abs(dy);
+ } else if (direction === 2) { // Down
+ if (dy <= 5) continue; // must be below
+ score = (dy * 2.0) - Math.abs(dx);
+ } else if (direction === 3) { // Left
+ if (dx >= -5) continue; // must be left
+ score = (-dx * 2.0) - Math.abs(dy);
+ }
+
+ if (score > bestScore) {
+ bestScore = score;
+ bestNeighbor = n;
+ }
+ }
+
+ return bestNeighbor || currentNode;
+}
From ed758812f88ac414dbb1190991fabbe750a3273d Mon Sep 17 00:00:00 2001
From: reze-dev <25588579+reze-dev@users.noreply.github.com>
Date: Sat, 26 Sep 2026 01:22:48 +0530
Subject: [PATCH 02/48] feat(radial): add data-driven RadialSettingsModel with
ctOS service bindings
---
.../surfaces/radial/RadialSettingsModel.qml | 757 ++++++++++++++++++
1 file changed, 757 insertions(+)
create mode 100644 shell/desktop/surfaces/radial/RadialSettingsModel.qml
diff --git a/shell/desktop/surfaces/radial/RadialSettingsModel.qml b/shell/desktop/surfaces/radial/RadialSettingsModel.qml
new file mode 100644
index 00000000..a806f05d
--- /dev/null
+++ b/shell/desktop/surfaces/radial/RadialSettingsModel.qml
@@ -0,0 +1,757 @@
+import QtQuick
+import Quickshell
+import "../../core"
+import "../../services"
+
+Item {
+ id: root
+
+ // Reactive counter to trigger view refreshes when settings mutate
+ property int revision: 0
+
+ Connections {
+ target: Settings
+ function onSettingsSaved(): void {
+ root.revision++;
+ }
+ }
+
+ Connections {
+ target: SystemMonitorService
+ function onTelemetryUpdated(): void {
+ root.revision++;
+ }
+ }
+
+ Connections {
+ target: NetworkService
+ function onNetworkStateChanged(): void {
+ root.revision++;
+ }
+ }
+
+ readonly property int categoryCount: categories.length
+
+ // 8 Categories arranged clockwise starting at 12 o'clock (-90 deg):
+ // 0: SYSTEM (-90 deg)
+ // 1: APPEARANCE (-45 deg)
+ // 2: DESKTOP (0 deg)
+ // 3: NETWORK (45 deg)
+ // 4: AUDIO (90 deg)
+ // 5: INPUT (135 deg)
+ // 6: POWER (180 deg)
+ // 7: SECURITY (-135 deg)
+ readonly property var categories: [
+ // =====================================================================
+ // 0. SYSTEM
+ // =====================================================================
+ {
+ id: "system",
+ name: "SYSTEM",
+ subtitle: "KERNEL TELEMETRY // SCHEDULER",
+ icon: "gear",
+ description: "Hardware telemetry monitors, procfs kernel scheduler, and CPU thread profiling matrix.",
+ nodes: [
+ {
+ id: "sys-core",
+ title: "KERNEL CORE",
+ subtitle: "SYS.01 // SCHEDULER",
+ icon: "gear",
+ description: "ctOS Linux kernel telemetry subsystem and process scheduler core.",
+ locked: false,
+ lockReason: "",
+ requires: [],
+ pos: { x: 70, y: 0 },
+ edges: ["sys-cpu-hex", "sys-ram-bar"],
+ controlType: "readonly",
+ value: function() { return SystemMonitorService.available ? 1 : 0; },
+ valueText: function() { return SystemMonitorService.available ? "ONLINE" : "OFFLINE"; },
+ execute: function() {}
+ },
+ {
+ id: "sys-cpu-hex",
+ title: "CPU HEX-GRID",
+ subtitle: "SYS.02 // CORES",
+ icon: "gear",
+ description: "Per-core CPU utilization hex matrix in the ambient desktop status bar.",
+ locked: false,
+ lockReason: "",
+ requires: ["sys-core"],
+ pos: { x: 230, y: -100 },
+ edges: ["sys-profiler"],
+ controlType: "toggle",
+ value: function() { return Settings.widgetCpuHexGridVisible; },
+ valueText: function() { return Settings.widgetCpuHexGridVisible ? "ENABLED" : "DISABLED"; },
+ execute: function() {
+ Settings.widgetCpuHexGridVisible = !Settings.widgetCpuHexGridVisible;
+ Settings.save();
+ root.revision++;
+ }
+ },
+ {
+ id: "sys-ram-bar",
+ title: "RAM BLOCK-BAR",
+ subtitle: "SYS.03 // MEMORY",
+ icon: "gear",
+ description: "Physical memory and swap page allocation telemetry bar widget.",
+ locked: false,
+ lockReason: "",
+ requires: ["sys-core"],
+ pos: { x: 230, y: 100 },
+ edges: [],
+ controlType: "toggle",
+ value: function() { return Settings.widgetRamBlockBarVisible; },
+ valueText: function() { return Settings.widgetRamBlockBarVisible ? "ENABLED" : "DISABLED"; },
+ execute: function() {
+ Settings.widgetRamBlockBarVisible = !Settings.widgetRamBlockBarVisible;
+ Settings.save();
+ root.revision++;
+ }
+ },
+ {
+ id: "sys-profiler",
+ title: "TARGET PROFILER",
+ subtitle: "SYS.04 // THREADS",
+ icon: "gear",
+ description: "Active system process profiler and execution latency inspector.",
+ locked: false,
+ lockReason: "",
+ requires: ["sys-cpu-hex"],
+ pos: { x: 390, y: -100 },
+ edges: [],
+ controlType: "toggle",
+ value: function() { return Settings.widgetTargetProfilerVisible; },
+ valueText: function() { return Settings.widgetTargetProfilerVisible ? "ENABLED" : "DISABLED"; },
+ execute: function() {
+ Settings.widgetTargetProfilerVisible = !Settings.widgetTargetProfilerVisible;
+ Settings.save();
+ root.revision++;
+ }
+ }
+ ]
+ },
+
+ // =====================================================================
+ // 1. APPEARANCE
+ // =====================================================================
+ {
+ id: "appearance",
+ name: "APPEARANCE",
+ subtitle: "VISUAL PIPELINE // MOTION",
+ icon: "brightness",
+ description: "Compositor styling engine, reduced motion dampening, and panel geometry dimensions.",
+ nodes: [
+ {
+ id: "app-engine",
+ title: "DISPLAY ENGINE",
+ subtitle: "APP.01 // THEME",
+ icon: "brightness",
+ description: "ctOS visual styling pipeline and industrial cyberpunk theme framework.",
+ locked: false,
+ lockReason: "",
+ requires: [],
+ pos: { x: 70, y: 0 },
+ edges: ["app-motion", "app-bar-height"],
+ controlType: "readonly",
+ value: function() { return 1; },
+ valueText: function() { return Settings.theme.toUpperCase(); },
+ execute: function() {}
+ },
+ {
+ id: "app-motion",
+ title: "TACTICAL MOTION",
+ subtitle: "APP.02 // KINETICS",
+ icon: "brightness",
+ description: "High-speed cybernetic UI animations, transitions, and kinetic physics.",
+ locked: false,
+ lockReason: "",
+ requires: ["app-engine"],
+ pos: { x: 230, y: -100 },
+ edges: ["app-palette"],
+ controlType: "toggle",
+ value: function() { return !Settings.reducedMotion; },
+ valueText: function() { return Settings.reducedMotion ? "REDUCED" : "FULL FX"; },
+ execute: function() {
+ Settings.reducedMotion = !Settings.reducedMotion;
+ Settings.save();
+ root.revision++;
+ }
+ },
+ {
+ id: "app-bar-height",
+ title: "BAR HEIGHT",
+ subtitle: "APP.03 // GEOMETRY",
+ icon: "brightness",
+ description: "Vertical pixel thickness of the top ambient system telemetry bar.",
+ locked: false,
+ lockReason: "",
+ requires: ["app-engine"],
+ pos: { x: 230, y: 100 },
+ edges: [],
+ controlType: "slider",
+ minVal: 28,
+ maxVal: 48,
+ step: 4,
+ value: function() { return Settings.barHeight; },
+ valueText: function() { return Settings.barHeight + " PX"; },
+ execute: function(val) {
+ if (typeof val !== "number" || isNaN(val)) {
+ val = Settings.barHeight + 4;
+ if (val > 48) val = 28;
+ }
+ Settings.barHeight = Math.max(28, Math.min(48, Math.round(val)));
+ Settings.save();
+ root.revision++;
+ }
+ },
+ {
+ id: "app-palette",
+ title: "ACID ACCENT",
+ subtitle: "APP.04 // COLORWAY",
+ icon: "brightness",
+ description: "Primary phosphor luminescence wavelength (Acid Green).",
+ locked: false,
+ lockReason: "",
+ requires: ["app-motion"],
+ pos: { x: 390, y: -100 },
+ edges: [],
+ controlType: "readonly",
+ value: function() { return 1; },
+ valueText: function() { return "ACID GREEN"; },
+ execute: function() {}
+ }
+ ]
+ },
+
+ // =====================================================================
+ // 2. DESKTOP
+ // =====================================================================
+ {
+ id: "desktop",
+ name: "DESKTOP",
+ subtitle: "WAYLAND SHELL // SURFACES",
+ icon: "check",
+ description: "Wayland compositor surfaces, Command Deck application launcher, and side hubs.",
+ nodes: [
+ {
+ id: "dt-compositor",
+ title: "WAYLAND CORE",
+ subtitle: "DT.01 // SERVER",
+ icon: "check",
+ description: "Native Wayland compositor session (Hyprland / Niri protocols).",
+ locked: false,
+ lockReason: "",
+ requires: [],
+ pos: { x: 70, y: 0 },
+ edges: ["dt-command-deck", "dt-system-rail"],
+ controlType: "readonly",
+ value: function() { return 1; },
+ valueText: function() { return SessionService.compositorName.toUpperCase(); },
+ execute: function() {}
+ },
+ {
+ id: "dt-command-deck",
+ title: "COMMAND DECK",
+ subtitle: "DT.02 // LAUNCHER",
+ icon: "check",
+ description: "Fuzzy search launcher and system action execution console.",
+ locked: false,
+ lockReason: "",
+ requires: ["dt-compositor"],
+ pos: { x: 230, y: -100 },
+ edges: ["dt-notifications"],
+ controlType: "toggle",
+ value: function() { return Settings.featuresCommandDeck; },
+ valueText: function() { return Settings.featuresCommandDeck ? "ENABLED" : "DISABLED"; },
+ execute: function() {
+ Settings.featuresCommandDeck = !Settings.featuresCommandDeck;
+ Settings.save();
+ root.revision++;
+ }
+ },
+ {
+ id: "dt-system-rail",
+ title: "SYSTEM RAIL",
+ subtitle: "DT.03 // SIDE HUB",
+ icon: "check",
+ description: "Collapsible side panel with hardware controls and audio routing.",
+ locked: false,
+ lockReason: "",
+ requires: ["dt-compositor"],
+ pos: { x: 230, y: 100 },
+ edges: [],
+ controlType: "toggle",
+ value: function() { return Settings.featuresSystemRail; },
+ valueText: function() { return Settings.featuresSystemRail ? "ENABLED" : "DISABLED"; },
+ execute: function() {
+ Settings.featuresSystemRail = !Settings.featuresSystemRail;
+ Settings.save();
+ root.revision++;
+ }
+ },
+ {
+ id: "dt-notifications",
+ title: "ALERT TOASTS",
+ subtitle: "DT.04 // NOTIFIER",
+ icon: "check",
+ description: "Tactical notification alert toasts and desktop signal daemon.",
+ locked: false,
+ lockReason: "",
+ requires: ["dt-command-deck"],
+ pos: { x: 390, y: -100 },
+ edges: [],
+ controlType: "toggle",
+ value: function() { return Settings.featuresNotifications; },
+ valueText: function() { return Settings.featuresNotifications ? "ENABLED" : "DISABLED"; },
+ execute: function() {
+ Settings.featuresNotifications = !Settings.featuresNotifications;
+ Settings.save();
+ root.revision++;
+ }
+ }
+ ]
+ },
+
+ // =====================================================================
+ // 3. NETWORK
+ // =====================================================================
+ {
+ id: "network",
+ name: "NETWORK",
+ subtitle: "INTERFACES // PACKET FLOW",
+ icon: "wifi",
+ description: "NetworkManager client daemon, packet flow throughput, and subnet node tracer.",
+ nodes: [
+ {
+ id: "net-core",
+ title: "LINK ADAPTER",
+ subtitle: "NET.01 // INTERFACE",
+ icon: "wifi",
+ description: "Primary network interface connection and transport controller.",
+ locked: false,
+ lockReason: "",
+ requires: [],
+ pos: { x: 70, y: 0 },
+ edges: ["net-flow", "net-tracer"],
+ controlType: "readonly",
+ value: function() { return NetworkService.isConnected ? 1 : 0; },
+ valueText: function() { return NetworkService.isConnected ? NetworkService.networkName : "OFFLINE"; },
+ execute: function() {}
+ },
+ {
+ id: "net-flow",
+ title: "FLOW MATRIX",
+ subtitle: "NET.02 // BANDWIDTH",
+ icon: "wifi",
+ description: "Real-time bandwidth throughput matrix in ambient status bar.",
+ locked: false,
+ lockReason: "",
+ requires: ["net-core"],
+ pos: { x: 230, y: -100 },
+ edges: ["net-sniffer"],
+ controlType: "toggle",
+ value: function() { return Settings.widgetNetworkFlowVisible; },
+ valueText: function() { return Settings.widgetNetworkFlowVisible ? "ENABLED" : "DISABLED"; },
+ execute: function() {
+ Settings.widgetNetworkFlowVisible = !Settings.widgetNetworkFlowVisible;
+ Settings.save();
+ root.revision++;
+ }
+ },
+ {
+ id: "net-tracer",
+ title: "SUBNET TRACER",
+ subtitle: "NET.03 // LATENCY",
+ icon: "wifi",
+ description: "Hop latency tracer and subnet target ping telemetry widget.",
+ locked: false,
+ lockReason: "",
+ requires: ["net-core"],
+ pos: { x: 230, y: 100 },
+ edges: [],
+ controlType: "toggle",
+ value: function() { return Settings.widgetNetworkTracerVisible; },
+ valueText: function() { return Settings.widgetNetworkTracerVisible ? "ENABLED" : "DISABLED"; },
+ execute: function() {
+ Settings.widgetNetworkTracerVisible = !Settings.widgetNetworkTracerVisible;
+ Settings.save();
+ root.revision++;
+ }
+ },
+ {
+ id: "net-sniffer",
+ title: "PACKET SNIFFER",
+ subtitle: "NET.04 // SURVEILLANCE",
+ icon: "wifi",
+ description: "Deep packet inspection and socket surveillance matrix.",
+ locked: true,
+ lockReason: "Requires raw CAP_NET_RAW / promiscuous socket privileges.",
+ requires: ["net-flow"],
+ pos: { x: 390, y: -100 },
+ edges: [],
+ controlType: "readonly",
+ value: function() { return 0; },
+ valueText: function() { return "RESTRICTED"; },
+ execute: function() {}
+ }
+ ]
+ },
+
+ // =====================================================================
+ // 4. AUDIO
+ // =====================================================================
+ {
+ id: "audio",
+ name: "AUDIO",
+ subtitle: "PIPEWIRE // SPECTRUM",
+ icon: "volume",
+ description: "PipeWire sound server output sinks, microphone input gate, and FFT spectrum.",
+ nodes: [
+ {
+ id: "audio-master",
+ title: "OUTPUT SINK",
+ subtitle: "AUD.01 // PIPEWIRE",
+ icon: "volume",
+ description: "PipeWire master output stream sink: " + (AudioService.sinkName || "Default"),
+ locked: false,
+ lockReason: "",
+ requires: [],
+ pos: { x: 70, y: 0 },
+ edges: ["audio-mute", "audio-spectrum"],
+ controlType: "slider",
+ minVal: 0,
+ maxVal: 100,
+ step: 5,
+ value: function() { return Math.round(AudioService.volume * 100); },
+ valueText: function() { return Math.round(AudioService.volume * 100) + "%"; },
+ execute: function(val) {
+ if (typeof val !== "number" || isNaN(val)) {
+ val = Math.round(AudioService.volume * 100) + 10;
+ if (val > 100) val = 0;
+ }
+ AudioService.setVolume(Math.max(0, Math.min(100, val)) / 100.0);
+ root.revision++;
+ }
+ },
+ {
+ id: "audio-mute",
+ title: "MUTE TOGGLE",
+ subtitle: "AUD.02 // ATTENUATION",
+ icon: "volume",
+ description: "Master output hardware gate toggle.",
+ locked: false,
+ lockReason: "",
+ requires: ["audio-master"],
+ pos: { x: 230, y: -100 },
+ edges: ["audio-dsp"],
+ controlType: "toggle",
+ value: function() { return !AudioService.muted; },
+ valueText: function() { return AudioService.muted ? "MUTED" : "ACTIVE"; },
+ execute: function() {
+ AudioService.toggleMute();
+ root.revision++;
+ }
+ },
+ {
+ id: "audio-spectrum",
+ title: "SPECTRUM SURVEILLANCE",
+ subtitle: "AUD.03 // FFT BARS",
+ icon: "volume",
+ description: "Real-time FFT audio surveillance frequency visualization widget.",
+ locked: false,
+ lockReason: "",
+ requires: ["audio-master"],
+ pos: { x: 230, y: 100 },
+ edges: [],
+ controlType: "toggle",
+ value: function() { return Settings.widgetAudioSurveillanceVisible; },
+ valueText: function() { return Settings.widgetAudioSurveillanceVisible ? "ENABLED" : "DISABLED"; },
+ execute: function() {
+ Settings.widgetAudioSurveillanceVisible = !Settings.widgetAudioSurveillanceVisible;
+ Settings.save();
+ root.revision++;
+ }
+ },
+ {
+ id: "audio-dsp",
+ title: "DSP SPATIALIZER",
+ subtitle: "AUD.04 // FILTER",
+ icon: "volume",
+ description: "Hardware acoustic spatializer and real-time noise cancellation matrix.",
+ locked: true,
+ lockReason: "DSP kernel pipeline locked by audio server driver.",
+ requires: ["audio-mute"],
+ pos: { x: 390, y: -100 },
+ edges: [],
+ controlType: "readonly",
+ value: function() { return 0; },
+ valueText: function() { return "RESTRICTED"; },
+ execute: function() {}
+ }
+ ]
+ },
+
+ // =====================================================================
+ // 5. INPUT
+ // =====================================================================
+ {
+ id: "input",
+ name: "INPUT",
+ subtitle: "LIBINPUT // HID DEVICES",
+ icon: "gear",
+ description: "Pointer acceleration, keyboard repeat rate, and multi-finger gestures.",
+ nodes: [
+ {
+ id: "in-engine",
+ title: "INPUT ENGINE",
+ subtitle: "INP.01 // LIBINPUT",
+ icon: "gear",
+ description: "Kernel evdev abstraction and libinput driver layer.",
+ locked: false,
+ lockReason: "",
+ requires: [],
+ pos: { x: 70, y: 0 },
+ edges: ["in-pointer", "in-repeat"],
+ controlType: "readonly",
+ value: function() { return 1; },
+ valueText: function() { return "ACTIVE"; },
+ execute: function() {}
+ },
+ {
+ id: "in-pointer",
+ title: "POINTER SENSE",
+ subtitle: "INP.02 // CURSOR",
+ icon: "gear",
+ description: "Pointer acceleration profile and sensitivity curves.",
+ locked: false,
+ lockReason: "",
+ requires: ["in-engine"],
+ pos: { x: 230, y: -100 },
+ edges: ["in-gestures"],
+ controlType: "action",
+ actionLabel: "CYCLE PROFILE",
+ value: function() { return 1; },
+ valueText: function() { return "FLAT"; },
+ execute: function() {
+ root.revision++;
+ }
+ },
+ {
+ id: "in-repeat",
+ title: "KEY REPEAT",
+ subtitle: "INP.03 // TYPEMATIC",
+ icon: "gear",
+ description: "Keyboard repeat delay and typematic strike frequency.",
+ locked: false,
+ lockReason: "",
+ requires: ["in-engine"],
+ pos: { x: 230, y: 100 },
+ edges: [],
+ controlType: "readonly",
+ value: function() { return 1; },
+ valueText: function() { return "25ms / 600ms"; },
+ execute: function() {}
+ },
+ {
+ id: "in-gestures",
+ title: "TOUCH GESTURES",
+ subtitle: "INP.04 // PRECISION",
+ icon: "gear",
+ description: "Multi-touch workspace navigation and boundary swipe recognition.",
+ locked: true,
+ lockReason: "No supported precision touchpad hardware detected.",
+ requires: ["in-pointer"],
+ pos: { x: 390, y: -100 },
+ edges: [],
+ controlType: "readonly",
+ value: function() { return 0; },
+ valueText: function() { return "RESTRICTED"; },
+ execute: function() {}
+ }
+ ]
+ },
+
+ // =====================================================================
+ // 6. POWER
+ // =====================================================================
+ {
+ id: "power",
+ name: "POWER",
+ subtitle: "UPOWER // GOVERNOR",
+ icon: "power",
+ description: "Energy governor, battery charge conservation, and display DPMS timeouts.",
+ nodes: [
+ {
+ id: "pwr-governor",
+ title: "ENERGY DAEMON",
+ subtitle: "PWR.01 // UPOWER",
+ icon: "power",
+ description: "UPower daemon connection and system power supply status.",
+ locked: false,
+ lockReason: "",
+ requires: [],
+ pos: { x: 70, y: 0 },
+ edges: ["pwr-supply", "pwr-sleep"],
+ controlType: "readonly",
+ value: function() { return PowerService.available ? 1 : 0; },
+ valueText: function() { return PowerService.stateText; },
+ execute: function() {}
+ },
+ {
+ id: "pwr-supply",
+ title: "BATTERY CAPACITY",
+ subtitle: "PWR.02 // CHARGE",
+ icon: "power",
+ description: "Lithium-ion energy storage cell state and charge level.",
+ locked: false,
+ lockReason: "",
+ requires: ["pwr-governor"],
+ pos: { x: 230, y: -100 },
+ edges: ["pwr-threshold"],
+ controlType: "readonly",
+ value: function() { return PowerService.isBatteryPresent ? PowerService.percentage : 100; },
+ valueText: function() { return PowerService.isBatteryPresent ? Math.round(PowerService.percentage) + "%" : "AC MAINS"; },
+ execute: function() {}
+ },
+ {
+ id: "pwr-sleep",
+ title: "IDLE BLANKING",
+ subtitle: "PWR.03 // DPMS",
+ icon: "power",
+ description: "Display DPMS timeout and screen power conservation.",
+ locked: false,
+ lockReason: "",
+ requires: ["pwr-governor"],
+ pos: { x: 230, y: 100 },
+ edges: [],
+ controlType: "readonly",
+ value: function() { return 1; },
+ valueText: function() { return "300 SEC"; },
+ execute: function() {}
+ },
+ {
+ id: "pwr-threshold",
+ title: "CHARGE THRESHOLD",
+ subtitle: "PWR.04 // CONSERVATION",
+ icon: "power",
+ description: "Firmware-level 80% battery longevity threshold limit.",
+ locked: true,
+ lockReason: "Requires ACPI battery charge threshold driver support.",
+ requires: ["pwr-supply"],
+ pos: { x: 390, y: -100 },
+ edges: [],
+ controlType: "readonly",
+ value: function() { return 0; },
+ valueText: function() { return "RESTRICTED"; },
+ execute: function() {}
+ }
+ ]
+ },
+
+ // =====================================================================
+ // 7. SECURITY
+ // =====================================================================
+ {
+ id: "security",
+ name: "SECURITY",
+ subtitle: "PAM AUTH // PRIVACY",
+ icon: "lock",
+ description: "Session lock supervisor, notification privacy filters, and encrypted vault.",
+ nodes: [
+ {
+ id: "sec-subsystem",
+ title: "SUPERVISOR",
+ subtitle: "SEC.01 // PAM",
+ icon: "lock",
+ description: "ctOS session security supervisor and authentication guard.",
+ locked: false,
+ lockReason: "",
+ requires: [],
+ pos: { x: 70, y: 0 },
+ edges: ["sec-lock", "sec-privacy"],
+ controlType: "readonly",
+ value: function() { return 1; },
+ valueText: function() { return "SECURE"; },
+ execute: function() {}
+ },
+ {
+ id: "sec-lock",
+ title: "SESSION LOCK",
+ subtitle: "SEC.02 // DISPLAY",
+ icon: "lock",
+ description: "Immediately engage loginctl lock screen overlay.",
+ locked: false,
+ lockReason: "",
+ requires: ["sec-subsystem"],
+ pos: { x: 230, y: -100 },
+ edges: ["sec-vault"],
+ controlType: "action",
+ actionLabel: "LOCK SESSION",
+ value: function() { return 1; },
+ valueText: function() { return "STANDBY"; },
+ execute: function() {
+ SessionService.lock();
+ root.revision++;
+ }
+ },
+ {
+ id: "sec-privacy",
+ title: "ALERT PRIVACY",
+ subtitle: "SEC.03 // COOLDOWN",
+ icon: "lock",
+ description: "Enforce anti-spam cooldown intervals on notification popups.",
+ locked: false,
+ lockReason: "",
+ requires: ["sec-subsystem"],
+ pos: { x: 230, y: 100 },
+ edges: [],
+ controlType: "toggle",
+ value: function() { return Settings.notificationCooldownSeconds > 0; },
+ valueText: function() { return Settings.notificationCooldownSeconds > 0 ? "ENABLED" : "DISABLED"; },
+ execute: function() {
+ Settings.notificationCooldownSeconds = (Settings.notificationCooldownSeconds > 0 ? 0 : 30);
+ Settings.save();
+ root.revision++;
+ }
+ },
+ {
+ id: "sec-vault",
+ title: "ENCRYPTED VAULT",
+ subtitle: "SEC.04 // LUKS-HSM",
+ icon: "lock",
+ description: "Hardware security module and encrypted credentials vault.",
+ locked: true,
+ lockReason: "Requires LUKS hardware token or biometric key-ring authorization.",
+ requires: ["sec-lock"],
+ pos: { x: 390, y: -100 },
+ edges: [],
+ controlType: "readonly",
+ value: function() { return 0; },
+ valueText: function() { return "RESTRICTED"; },
+ execute: function() {}
+ }
+ ]
+ }
+ ]
+
+ function getCategory(index) {
+ if (index < 0 || index >= categories.length) return null;
+ return categories[index];
+ }
+
+ function getCategoryById(catId) {
+ for (var i = 0; i < categories.length; ++i) {
+ if (categories[i].id === catId) return categories[i];
+ }
+ return null;
+ }
+
+ function getNode(catIndex, nodeId) {
+ var cat = getCategory(catIndex);
+ if (!cat || !cat.nodes) return null;
+ for (var i = 0; i < cat.nodes.length; ++i) {
+ if (cat.nodes[i].id === nodeId) return cat.nodes[i];
+ }
+ return null;
+ }
+}
From 68bc79294319a736f5e9676e4ecf0725abddfcd7 Mon Sep 17 00:00:00 2001
From: reze-dev <25588579+reze-dev@users.noreply.github.com>
Date: Sat, 26 Sep 2026 01:22:52 +0530
Subject: [PATCH 03/48] feat(radial): implement RadialSegment annular sector
component
---
.../desktop/surfaces/radial/RadialSegment.qml | 142 ++++++++++++++++++
1 file changed, 142 insertions(+)
create mode 100644 shell/desktop/surfaces/radial/RadialSegment.qml
diff --git a/shell/desktop/surfaces/radial/RadialSegment.qml b/shell/desktop/surfaces/radial/RadialSegment.qml
new file mode 100644
index 00000000..26f76766
--- /dev/null
+++ b/shell/desktop/surfaces/radial/RadialSegment.qml
@@ -0,0 +1,142 @@
+import QtQuick
+import QtQuick.Shapes
+import "../../core"
+import "../components"
+import "RadialGeometry.js" as RadialGeometry
+
+Item {
+ id: root
+
+ // =========================================================================
+ // Properties
+ // =========================================================================
+
+ property int index: 0
+ property string categoryId: ""
+ property string categoryName: ""
+ property string iconName: "gear"
+
+ property real cx: width / 2
+ property real cy: height / 2
+ property real baseInnerRadius: 100
+ property real baseOuterRadius: 170
+ property real startAngle: -90.0
+ property real endAngle: -45.0
+ property real wheelRotation: 0.0
+
+ property bool isFocused: false
+ property bool isSelected: false
+ property bool isDimmed: false
+
+ readonly property real centerAngle: (startAngle + endAngle) / 2.0
+
+ // Animated dynamic radii for hover expansion
+ property real effectiveInnerRadius: isFocused ? (baseInnerRadius - 4) : baseInnerRadius
+ property real effectiveOuterRadius: isFocused ? (baseOuterRadius + 12) : baseOuterRadius
+
+ Behavior on effectiveInnerRadius {
+ NumberAnimation {
+ duration: Theme.durationNormal
+ easing.type: Easing.OutCubic
+ }
+ }
+
+ Behavior on effectiveOuterRadius {
+ NumberAnimation {
+ duration: Theme.durationNormal
+ easing.type: Easing.OutCubic
+ }
+ }
+
+ // Dynamic Opacity for Dimmed State
+ opacity: isDimmed ? 0.15 : 1.0
+ Behavior on opacity {
+ NumberAnimation {
+ duration: Theme.durationSlow
+ easing.type: Easing.OutCubic
+ }
+ }
+
+ // Centered icon coordinates along the radial sector ray
+ readonly property var iconCenter: RadialGeometry.pointOnCircle(
+ root.cx,
+ root.cy,
+ (root.effectiveInnerRadius + root.effectiveOuterRadius) / 2.0,
+ root.centerAngle
+ )
+
+ // Signals
+ signal clicked()
+ signal hovered()
+
+ // Annular Sector Vector Shape
+ Shape {
+ id: sectorShape
+ anchors.fill: parent
+ preferredRendererType: Shape.CurveRenderer
+
+ ShapePath {
+ id: sectorPath
+ strokeWidth: (root.isFocused || root.isSelected) ? 2.0 : 1.0
+ strokeColor: root.isFocused ? Theme.gray50 : (root.isSelected ? Theme.acidGreen : Theme.gray700)
+ fillColor: root.isFocused ? Theme.gray50 : (root.isSelected ? Theme.gray800 : Theme.gray900)
+ capStyle: ShapePath.FlatCap
+ joinStyle: ShapePath.MiterJoin
+
+ Behavior on fillColor {
+ ColorAnimation { duration: Theme.durationFast }
+ }
+ Behavior on strokeColor {
+ ColorAnimation { duration: Theme.durationFast }
+ }
+
+ PathSvg {
+ path: RadialGeometry.createSectorSvgPath(
+ root.cx,
+ root.cy,
+ root.effectiveInnerRadius,
+ root.effectiveOuterRadius,
+ root.startAngle,
+ root.endAngle
+ )
+ }
+ }
+ }
+
+ // Centered Category Icon with Rotation Compensation
+ Item {
+ id: iconContainer
+ x: root.iconCenter.x - width / 2
+ y: root.iconCenter.y - height / 2
+ width: 36
+ height: 36
+
+ // Counter-rotate by wheel rotation so icon stays strictly upright
+ rotation: -root.wheelRotation
+
+ CtosIcon {
+ id: categoryIcon
+ anchors.centerIn: parent
+ size: 22
+ name: root.iconName
+ color: root.isFocused ? Theme.gray900 : (root.isSelected ? Theme.acidGreen : Theme.gray200)
+
+ Behavior on color {
+ ColorAnimation { duration: Theme.durationFast }
+ }
+ }
+
+ MouseArea {
+ anchors.fill: parent
+ cursorShape: Qt.PointingHandCursor
+ hoverEnabled: true
+
+ onEntered: {
+ root.hovered();
+ }
+ onClicked: {
+ root.clicked();
+ }
+ }
+ }
+}
From 31a11f26d92e1d73a757b642f4ca1d9088692cb5 Mon Sep 17 00:00:00 2001
From: reze-dev <25588579+reze-dev@users.noreply.github.com>
Date: Sat, 26 Sep 2026 01:22:57 +0530
Subject: [PATCH 04/48] feat(radial): implement SkillNode and SkillEdge graph
components
---
shell/desktop/surfaces/radial/SkillEdge.qml | 82 +++++++++
shell/desktop/surfaces/radial/SkillNode.qml | 179 ++++++++++++++++++++
2 files changed, 261 insertions(+)
create mode 100644 shell/desktop/surfaces/radial/SkillEdge.qml
create mode 100644 shell/desktop/surfaces/radial/SkillNode.qml
diff --git a/shell/desktop/surfaces/radial/SkillEdge.qml b/shell/desktop/surfaces/radial/SkillEdge.qml
new file mode 100644
index 00000000..6a6b1b39
--- /dev/null
+++ b/shell/desktop/surfaces/radial/SkillEdge.qml
@@ -0,0 +1,82 @@
+import QtQuick
+import QtQuick.Shapes
+import "../../core"
+
+Item {
+ id: root
+
+ // =========================================================================
+ // Properties
+ // =========================================================================
+
+ property real x1: 0.0
+ property real y1: 0.0
+ property real x2: 0.0
+ property real y2: 0.0
+
+ property bool isActive: false
+ property bool isPreview: false
+ property int pulseDuration: Theme.durationSlow
+
+ property real progress: 0.0
+
+ anchors.fill: parent
+
+ function triggerPulse() {
+ if (!isPreview) {
+ pulseAnim.restart();
+ }
+ }
+
+ // Base Connecting Vector Line
+ Shape {
+ id: lineShape
+ anchors.fill: parent
+ preferredRendererType: Shape.CurveRenderer
+
+ ShapePath {
+ strokeWidth: root.isPreview ? 1.0 : (root.isActive ? 2.0 : 1.2)
+ strokeColor: root.isActive ? Theme.acidGreen : (root.isPreview ? Theme.gray700 : Theme.gray600)
+ fillColor: "transparent"
+ capStyle: ShapePath.RoundCap
+
+ Behavior on strokeColor {
+ ColorAnimation { duration: Theme.durationFast }
+ }
+ Behavior on strokeWidth {
+ NumberAnimation { duration: Theme.durationFast }
+ }
+
+ startX: root.x1
+ startY: root.y1
+
+ PathLine {
+ x: root.x2
+ y: root.y2
+ }
+ }
+ }
+
+ // Traveling Signal Packet Animation
+ NumberAnimation {
+ id: pulseAnim
+ target: root
+ property: "progress"
+ from: 0.0
+ to: 1.0
+ duration: root.pulseDuration
+ easing.type: Easing.InOutQuad
+ }
+
+ // Traveling Signal Packet Dot
+ Rectangle {
+ id: signalPacket
+ width: 6
+ height: 6
+ radius: Theme.radiusPill
+ color: Theme.acidGreen
+ visible: pulseAnim.running && !root.isPreview
+ x: root.x1 + root.progress * (root.x2 - root.x1) - width / 2
+ y: root.y1 + root.progress * (root.y2 - root.y1) - height / 2
+ }
+}
diff --git a/shell/desktop/surfaces/radial/SkillNode.qml b/shell/desktop/surfaces/radial/SkillNode.qml
new file mode 100644
index 00000000..eda20312
--- /dev/null
+++ b/shell/desktop/surfaces/radial/SkillNode.qml
@@ -0,0 +1,179 @@
+import QtQuick
+import "../../core"
+import "../components"
+
+Item {
+ id: root
+
+ // =========================================================================
+ // Properties
+ // =========================================================================
+
+ property string nodeId: ""
+ property string title: ""
+ property string subtitle: ""
+ property string iconName: "gear"
+
+ property bool locked: false
+ property bool isSelected: false
+ property bool isPreview: false
+ property bool isHovered: false
+ property bool isActive: false
+
+ property real nodeSize: isPreview ? 20 : 48
+ property real screenX: x + width / 2
+ property real screenY: y + height / 2
+
+ width: nodeSize
+ height: nodeSize
+
+ signal clicked()
+ signal hovered()
+
+ function triggerPulse() {
+ pulseAnim.restart();
+ }
+
+ onIsSelectedChanged: {
+ if (isSelected && !isPreview) {
+ triggerPulse();
+ }
+ }
+
+ // Expanding Concentric Signal Pulse Wave
+ Rectangle {
+ id: pulseRing
+ anchors.centerIn: parent
+ width: root.width
+ height: root.height
+ radius: Theme.radiusPill
+ color: "transparent"
+ border.color: root.locked ? Theme.warningRed : Theme.acidGreen
+ border.width: 2
+ opacity: 0.0
+ scale: 1.0
+ visible: !root.isPreview
+
+ ParallelAnimation {
+ id: pulseAnim
+ NumberAnimation {
+ target: pulseRing
+ property: "scale"
+ from: 1.0
+ to: 1.8
+ duration: Theme.durationSlow
+ easing.type: Easing.OutCubic
+ }
+ NumberAnimation {
+ target: pulseRing
+ property: "opacity"
+ from: 0.8
+ to: 0.0
+ duration: Theme.durationSlow
+ easing.type: Easing.OutCubic
+ }
+ }
+ }
+
+ // Outer Selection / Status Ring
+ Rectangle {
+ id: outerRing
+ anchors.centerIn: parent
+ width: root.width + (root.isPreview ? 4 : 8)
+ height: root.height + (root.isPreview ? 4 : 8)
+ radius: Theme.radiusPill
+ color: "transparent"
+ border.width: root.isSelected ? 2 : (root.isHovered ? 1 : 0)
+ border.color: root.locked ? Theme.warningRed : (root.isSelected ? Theme.acidGreen : Theme.gray200)
+ visible: (root.isSelected || root.isHovered) && !root.isPreview
+
+ Behavior on border.color {
+ ColorAnimation { duration: Theme.durationFast }
+ }
+ }
+
+ // Main Node Disc
+ Rectangle {
+ id: nodeDisc
+ anchors.fill: parent
+ radius: Theme.radiusPill
+ color: root.isSelected ? Theme.gray800 : Theme.gray900
+ border.width: root.isSelected ? 2 : 1
+ border.color: root.locked ? Theme.warningRed : (root.isSelected ? (root.isPreview ? Theme.gray50 : Theme.acidGreen) : (root.isHovered ? Theme.gray200 : (root.isPreview ? Theme.gray800 : Theme.gray700)))
+
+ Behavior on border.color {
+ ColorAnimation { duration: Theme.durationFast }
+ }
+ Behavior on color {
+ ColorAnimation { duration: Theme.durationFast }
+ }
+
+ // Center Node Icon
+ CtosIcon {
+ id: nodeIcon
+ anchors.centerIn: parent
+ size: root.isPreview ? 12 : Math.max(16, root.nodeSize * 0.44)
+ name: root.locked ? "lock" : root.iconName
+ color: root.locked ? Theme.warningRed : (root.isSelected ? Theme.gray50 : (root.isHovered ? Theme.gray200 : (root.isPreview ? Theme.gray800 : Theme.gray500)))
+ visible: !root.isPreview || root.isSelected
+
+ Behavior on color {
+ ColorAnimation { duration: Theme.durationFast }
+ }
+ }
+
+ // Active State Indicator Dot
+ Rectangle {
+ id: activeDot
+ width: root.isPreview ? 4 : 6
+ height: root.isPreview ? 4 : 6
+ radius: Theme.radiusPill
+ color: Theme.acidGreen
+ anchors.bottom: parent.bottom
+ anchors.right: parent.right
+ anchors.margins: root.isPreview ? 1 : 3
+ visible: root.isActive && !root.locked
+ }
+
+ // Lock Badge Mini Indicator
+ Rectangle {
+ id: lockBadge
+ width: 14
+ height: 14
+ radius: Theme.radiusPill
+ color: Theme.gray900
+ border.color: Theme.warningRed
+ border.width: 1
+ anchors.top: parent.top
+ anchors.right: parent.right
+ anchors.margins: -2
+ visible: root.locked && !root.isPreview
+
+ CtosIcon {
+ anchors.centerIn: parent
+ size: 8
+ name: "lock"
+ color: Theme.warningRed
+ }
+ }
+ }
+
+ // Interactive Hover and Click Handler
+ MouseArea {
+ id: nodeMouseArea
+ anchors.fill: parent
+ hoverEnabled: true
+ cursorShape: Qt.PointingHandCursor
+
+ onEntered: {
+ root.isHovered = true;
+ root.hovered();
+ }
+ onExited: {
+ root.isHovered = false;
+ }
+ onClicked: {
+ root.clicked();
+ }
+ }
+}
From af19d7992830b1a926de779faeb9288b6c91ea82 Mon Sep 17 00:00:00 2001
From: reze-dev <25588579+reze-dev@users.noreply.github.com>
Date: Sat, 26 Sep 2026 01:23:07 +0530
Subject: [PATCH 05/48] feat(radial): add SkillTree graph renderer with preview
and cascade layouts
---
shell/desktop/surfaces/radial/SkillTree.qml | 313 ++++++++++++++++++++
1 file changed, 313 insertions(+)
create mode 100644 shell/desktop/surfaces/radial/SkillTree.qml
diff --git a/shell/desktop/surfaces/radial/SkillTree.qml b/shell/desktop/surfaces/radial/SkillTree.qml
new file mode 100644
index 00000000..a2e8ae14
--- /dev/null
+++ b/shell/desktop/surfaces/radial/SkillTree.qml
@@ -0,0 +1,313 @@
+pragma ComponentBehavior: Bound
+import QtQuick
+import "../../core"
+import "RadialGeometry.js" as RadialGeometry
+
+Item {
+ id: root
+
+ // =========================================================================
+ // Properties
+ // =========================================================================
+
+ property var model: null
+ property int focusedCategoryIndex: 0
+ property string selectedNodeId: ""
+ property bool isExpanded: false
+
+ property real wheelCenterX: width / 2
+ property real wheelCenterY: height / 2
+ property real branchOriginX: 300
+ property real branchOriginY: height / 2
+
+ signal nodeSelected(string nodeId)
+ signal nodeHovered(string nodeId)
+ signal nodeActivated(string nodeId)
+
+ // Track active nodes with calculated screen coordinates for spatial navigation
+ readonly property var activeNodes: {
+ if (!model || !isExpanded) return [];
+ var cat = model.getCategory(focusedCategoryIndex);
+ if (!cat || !cat.nodes) return [];
+ var list = [];
+ for (var i = 0; i < cat.nodes.length; ++i) {
+ var n = cat.nodes[i];
+ list.push({
+ id: n.id,
+ title: n.title,
+ subtitle: n.subtitle,
+ icon: n.icon,
+ locked: n.locked,
+ lockReason: n.lockReason,
+ controlType: n.controlType,
+ pos: n.pos,
+ screenX: root.branchOriginX + n.pos.x,
+ screenY: root.branchOriginY + n.pos.y
+ });
+ }
+ return list;
+ }
+
+ // =========================================================================
+ // 1. Preview Trees (Root Mode)
+ // 8 Subtrees Radiating Outward Around Wheel
+ // =========================================================================
+
+ Item {
+ id: previewTreesContainer
+ anchors.fill: parent
+ opacity: root.isExpanded ? 0.0 : 1.0
+ visible: opacity > 0.01
+
+ Behavior on opacity {
+ NumberAnimation { duration: Theme.durationSlow; easing.type: Easing.OutCubic }
+ }
+
+ Repeater {
+ model: 8
+ delegate: Item {
+ id: previewSubtree
+ required property int index
+ readonly property int categoryIdx: previewSubtree.index
+ readonly property real angleDeg: -90.0 + previewSubtree.categoryIdx * 45.0
+ readonly property bool isCatFocused: root.focusedCategoryIndex === previewSubtree.categoryIdx
+ readonly property var cat: (root.model && root.model.categories[previewSubtree.categoryIdx]) ? root.model.categories[previewSubtree.categoryIdx] : null
+
+ // Subtree polar anchor points
+ readonly property var r0Point: RadialGeometry.pointOnCircle(root.wheelCenterX, root.wheelCenterY, 175, previewSubtree.angleDeg)
+ readonly property var r1Point: RadialGeometry.pointOnCircle(root.wheelCenterX, root.wheelCenterY, 215, previewSubtree.angleDeg)
+ readonly property var c1Point: RadialGeometry.pointOnCircle(root.wheelCenterX, root.wheelCenterY, 260, previewSubtree.angleDeg - 8.0)
+ readonly property var c2Point: RadialGeometry.pointOnCircle(root.wheelCenterX, root.wheelCenterY, 260, previewSubtree.angleDeg + 8.0)
+ readonly property var c3Point: RadialGeometry.pointOnCircle(root.wheelCenterX, root.wheelCenterY, 305, previewSubtree.angleDeg - 12.0)
+
+ // Trunk edge
+ SkillEdge {
+ x1: previewSubtree.r0Point.x
+ y1: previewSubtree.r0Point.y
+ x2: previewSubtree.r1Point.x
+ y2: previewSubtree.r1Point.y
+ isActive: previewSubtree.isCatFocused
+ isPreview: true
+ }
+
+ // Branch edge 1
+ SkillEdge {
+ x1: previewSubtree.r1Point.x
+ y1: previewSubtree.r1Point.y
+ x2: previewSubtree.c1Point.x
+ y2: previewSubtree.c1Point.y
+ isActive: previewSubtree.isCatFocused
+ isPreview: true
+ }
+
+ // Branch edge 2
+ SkillEdge {
+ x1: previewSubtree.r1Point.x
+ y1: previewSubtree.r1Point.y
+ x2: previewSubtree.c2Point.x
+ y2: previewSubtree.c2Point.y
+ isActive: previewSubtree.isCatFocused
+ isPreview: true
+ }
+
+ // Branch edge 3 (Leaf extension)
+ SkillEdge {
+ x1: previewSubtree.c1Point.x
+ y1: previewSubtree.c1Point.y
+ x2: previewSubtree.c3Point.x
+ y2: previewSubtree.c3Point.y
+ isActive: previewSubtree.isCatFocused
+ isPreview: true
+ }
+
+ // Root Preview Node
+ SkillNode {
+ x: previewSubtree.r1Point.x - width / 2
+ y: previewSubtree.r1Point.y - height / 2
+ isPreview: true
+ isSelected: previewSubtree.isCatFocused
+ iconName: previewSubtree.cat ? previewSubtree.cat.icon : "gear"
+ }
+
+ // Child Preview Node 1
+ SkillNode {
+ x: previewSubtree.c1Point.x - width / 2
+ y: previewSubtree.c1Point.y - height / 2
+ isPreview: true
+ isSelected: previewSubtree.isCatFocused
+ iconName: (previewSubtree.cat && previewSubtree.cat.nodes && previewSubtree.cat.nodes[1]) ? previewSubtree.cat.nodes[1].icon : (previewSubtree.cat ? previewSubtree.cat.icon : "gear")
+ }
+
+ // Child Preview Node 2
+ SkillNode {
+ x: previewSubtree.c2Point.x - width / 2
+ y: previewSubtree.c2Point.y - height / 2
+ isPreview: true
+ isSelected: previewSubtree.isCatFocused
+ iconName: (previewSubtree.cat && previewSubtree.cat.nodes && previewSubtree.cat.nodes[2]) ? previewSubtree.cat.nodes[2].icon : (previewSubtree.cat ? previewSubtree.cat.icon : "gear")
+ }
+
+ // Child Preview Node 3
+ SkillNode {
+ x: previewSubtree.c3Point.x - width / 2
+ y: previewSubtree.c3Point.y - height / 2
+ isPreview: true
+ isSelected: previewSubtree.isCatFocused
+ iconName: (previewSubtree.cat && previewSubtree.cat.nodes && previewSubtree.cat.nodes[3]) ? previewSubtree.cat.nodes[3].icon : (previewSubtree.cat ? previewSubtree.cat.icon : "gear")
+ }
+ }
+ }
+ }
+
+ // =========================================================================
+ // 2. Expanded Branch (Interactive Mode)
+ // Cascading Horizontal Graph Extending From Left-Anchored Wheel
+ // =========================================================================
+
+ Item {
+ id: expandedBranchContainer
+ anchors.fill: parent
+ opacity: root.isExpanded ? 1.0 : 0.0
+ visible: opacity > 0.01
+
+ Behavior on opacity {
+ NumberAnimation { duration: Theme.durationSlow; easing.type: Easing.OutCubic }
+ }
+
+ // Horizontal Anchor Ray from Wheel to Root Node
+ SkillEdge {
+ id: anchorRay
+ x1: root.branchOriginX
+ y1: root.branchOriginY
+ x2: root.branchOriginX + 70
+ y2: root.branchOriginY
+ isActive: true
+ isPreview: false
+ }
+
+ Connections {
+ target: root
+ function onIsExpandedChanged(): void {
+ if (root.isExpanded) {
+ anchorRay.triggerPulse();
+ }
+ }
+ function onNodeSelected(nodeId): void {
+ var cat = root.model ? root.model.getCategory(root.focusedCategoryIndex) : null;
+ if (cat && cat.nodes && cat.nodes.length > 0 && cat.nodes[0].id === nodeId) {
+ anchorRay.triggerPulse();
+ }
+ }
+ }
+
+ // Inter-Node Edges
+ Repeater {
+ id: edgesRepeater
+ model: {
+ if (!root.model || !root.isExpanded) return [];
+ var cat = root.model.getCategory(root.focusedCategoryIndex);
+ if (!cat || !cat.nodes) return [];
+ var edgesList = [];
+ for (var i = 0; i < cat.nodes.length; ++i) {
+ var parentNode = cat.nodes[i];
+ if (!parentNode.edges) continue;
+ for (var j = 0; j < parentNode.edges.length; ++j) {
+ var childId = parentNode.edges[j];
+ var childNode = root.model.getNode(root.focusedCategoryIndex, childId);
+ if (childNode) {
+ edgesList.push({
+ parentId: parentNode.id,
+ childId: childId,
+ x1: root.branchOriginX + parentNode.pos.x,
+ y1: root.branchOriginY + parentNode.pos.y,
+ x2: root.branchOriginX + childNode.pos.x,
+ y2: root.branchOriginY + childNode.pos.y
+ });
+ }
+ }
+ }
+ return edgesList;
+ }
+
+ delegate: SkillEdge {
+ id: edgeItem
+ required property var modelData
+ x1: edgeItem.modelData.x1
+ y1: edgeItem.modelData.y1
+ x2: edgeItem.modelData.x2
+ y2: edgeItem.modelData.y2
+ isActive: root.selectedNodeId === edgeItem.modelData.childId || root.selectedNodeId === edgeItem.modelData.parentId
+ isPreview: false
+
+ Connections {
+ target: root
+ function onNodeSelected(nodeId): void {
+ if (nodeId === edgeItem.modelData.childId) {
+ edgeItem.triggerPulse();
+ }
+ }
+ }
+ }
+ }
+
+ // Graph Nodes
+ Repeater {
+ id: nodesRepeater
+ model: {
+ if (!root.model || !root.isExpanded) return [];
+ var cat = root.model.getCategory(root.focusedCategoryIndex);
+ return (cat && cat.nodes) ? cat.nodes : [];
+ }
+
+ delegate: Item {
+ id: nodeWrapper
+ required property var modelData
+ required property int index
+ x: root.branchOriginX + nodeWrapper.modelData.pos.x - 24 + cascadeOffset.offset
+ y: root.branchOriginY + nodeWrapper.modelData.pos.y - 24
+ width: 48
+ height: 48
+
+ // Staggered cascade entrance on branch expansion
+ QtObject {
+ id: cascadeOffset
+ property real offset: 0.0
+ }
+
+ NumberAnimation {
+ target: cascadeOffset
+ property: "offset"
+ from: -30.0
+ to: 0.0
+ duration: Theme.durationSlow + (nodeWrapper.index * 60)
+ running: root.isExpanded
+ easing.type: Easing.OutBack
+ }
+
+ SkillNode {
+ id: skillNode
+ anchors.centerIn: parent
+ nodeId: nodeWrapper.modelData.id
+ title: nodeWrapper.modelData.title
+ subtitle: nodeWrapper.modelData.subtitle
+ iconName: nodeWrapper.modelData.icon
+ locked: nodeWrapper.modelData.locked
+ isSelected: root.selectedNodeId === nodeWrapper.modelData.id
+ isActive: {
+ var _ = root.model ? root.model.revision : 0;
+ return typeof nodeWrapper.modelData.value === "function" && Boolean(nodeWrapper.modelData.value());
+ }
+
+ onClicked: {
+ root.selectedNodeId = nodeWrapper.modelData.id;
+ root.nodeSelected(nodeWrapper.modelData.id);
+ }
+
+ onHovered: {
+ root.nodeHovered(nodeWrapper.modelData.id);
+ }
+ }
+ }
+ }
+ }
+}
From 43aa82da913e2fe7b83ceba0db25e1302d1562de Mon Sep 17 00:00:00 2001
From: reze-dev <25588579+reze-dev@users.noreply.github.com>
Date: Sat, 26 Sep 2026 01:23:11 +0530
Subject: [PATCH 06/48] feat(radial): implement CircularSettingsMenu with
continuous rotation and translation
---
.../surfaces/radial/CircularSettingsMenu.qml | 178 ++++++++++++++++++
1 file changed, 178 insertions(+)
create mode 100644 shell/desktop/surfaces/radial/CircularSettingsMenu.qml
diff --git a/shell/desktop/surfaces/radial/CircularSettingsMenu.qml b/shell/desktop/surfaces/radial/CircularSettingsMenu.qml
new file mode 100644
index 00000000..e76b4ced
--- /dev/null
+++ b/shell/desktop/surfaces/radial/CircularSettingsMenu.qml
@@ -0,0 +1,178 @@
+pragma ComponentBehavior: Bound
+import QtQuick
+import "../../core"
+import "RadialGeometry.js" as RadialGeometry
+
+Item {
+ id: root
+
+ // =========================================================================
+ // Properties
+ // =========================================================================
+
+ property var model: null
+ property int focusedIndex: 0
+ property bool isExpanded: false
+
+ property real baseDiameter: 360
+ property real innerRadius: 95
+ property real outerRadius: 170
+ property real leftAnchorX: 180
+
+ // Animated Wheel Center Coordinates
+ property real targetCenterX: isExpanded ? leftAnchorX : (width / 2)
+ property real wheelCenterX: targetCenterX
+ property real wheelCenterY: height / 2
+
+ Behavior on wheelCenterX {
+ NumberAnimation {
+ duration: Theme.durationSlow
+ easing.type: Easing.OutCubic
+ }
+ }
+
+ // Target Rotation: Aligns selected category to 0 deg (pointing right)
+ readonly property real selectedBaseAngle: -90.0 + focusedIndex * 45.0
+ readonly property real desiredAngle: isExpanded ? RadialGeometry.normalizeAngle(-selectedBaseAngle) : 0.0
+ property real targetWheelRotation: desiredAngle
+ property real wheelRotation: targetWheelRotation
+
+ Behavior on wheelRotation {
+ NumberAnimation {
+ duration: Theme.durationSlow
+ easing.type: Easing.OutCubic
+ }
+ }
+
+ onDesiredAngleChanged: {
+ var diff = RadialGeometry.angleDifference(root.wheelRotation, root.desiredAngle);
+ root.targetWheelRotation = root.wheelRotation + diff;
+ }
+
+ // Signals
+ signal categoryClicked(int index)
+ signal categoryHovered(int index)
+
+ // Rotating Wheel Container
+ Item {
+ id: wheelContainer
+ x: root.wheelCenterX - width / 2
+ y: root.wheelCenterY - height / 2
+ width: root.baseDiameter + 40
+ height: root.baseDiameter + 40
+ rotation: root.wheelRotation
+ transformOrigin: Item.Center
+
+ // 8 Annular Sectors
+ Repeater {
+ model: 8
+ delegate: RadialSegment {
+ id: segItem
+ width: wheelContainer.width
+ height: wheelContainer.height
+ index: index
+ categoryId: (root.model && root.model.categories[index]) ? root.model.categories[index].id : ""
+ categoryName: (root.model && root.model.categories[index]) ? root.model.categories[index].name : ""
+ iconName: (root.model && root.model.categories[index]) ? root.model.categories[index].icon : "gear"
+ cx: wheelContainer.width / 2
+ cy: wheelContainer.height / 2
+ baseInnerRadius: root.innerRadius
+ baseOuterRadius: root.outerRadius
+ startAngle: -90.0 + index * 45.0 - 21.5
+ endAngle: -90.0 + index * 45.0 + 21.5
+ wheelRotation: root.wheelRotation
+ isFocused: root.focusedIndex === index && !root.isExpanded
+ isSelected: root.focusedIndex === index && root.isExpanded
+ isDimmed: root.isExpanded && root.focusedIndex !== index
+
+ onClicked: {
+ root.categoryClicked(index);
+ }
+ onHovered: {
+ root.categoryHovered(index);
+ }
+ }
+ }
+ }
+
+ // Center Circular HUD Hub
+ Item {
+ id: centerHub
+ x: root.wheelCenterX - width / 2
+ y: root.wheelCenterY - height / 2
+ width: root.innerRadius * 2 - 10
+ height: root.innerRadius * 2 - 10
+
+ // Hub Disc
+ Rectangle {
+ anchors.fill: parent
+ radius: Theme.radiusPill
+ color: Theme.gray900
+ border.color: root.isExpanded ? Theme.acidGreen : Theme.gray700
+ border.width: 1
+
+ Behavior on border.color {
+ ColorAnimation { duration: Theme.durationFast }
+ }
+
+ MouseArea {
+ anchors.fill: parent
+ cursorShape: Qt.PointingHandCursor
+ hoverEnabled: true
+ onClicked: {
+ if (root.isExpanded) {
+ root.isExpanded = false;
+ } else {
+ root.categoryClicked(root.focusedIndex);
+ }
+ }
+ }
+ }
+
+ // Concentric Inner Tech Ring
+ Rectangle {
+ anchors.centerIn: parent
+ width: parent.width - 24
+ height: parent.height - 24
+ radius: Theme.radiusPill
+ color: "transparent"
+ border.color: Theme.gray800
+ border.width: 1
+ }
+
+ // Center Content Text (Active Category Readout)
+ Column {
+ anchors.centerIn: parent
+ width: parent.width - 30
+ spacing: 4
+ opacity: root.isExpanded ? 0.3 : 1.0
+
+ Behavior on opacity {
+ NumberAnimation { duration: Theme.durationSlow; easing.type: Easing.OutCubic }
+ }
+
+ Text {
+ width: parent.width
+ horizontalAlignment: Text.AlignHCenter
+ text: (root.model && root.model.categories[root.focusedIndex]) ? root.model.categories[root.focusedIndex].name : "SYSTEM"
+ font.family: Theme.fontFamilyMonospace
+ font.pixelSize: Theme.fontSizeTitle
+ font.weight: Theme.fontWeightBold
+ color: Theme.textPrimary
+ elide: Text.ElideRight
+ }
+
+ Text {
+ width: parent.width
+ horizontalAlignment: Text.AlignHCenter
+ text: (root.model && root.model.categories[root.focusedIndex]) ? root.model.categories[root.focusedIndex].subtitle : ""
+ font.family: Theme.fontFamilyMonospace
+ font.pixelSize: Theme.fontSizeCaption
+ color: Theme.acidGreen
+ wrapMode: Text.WordWrap
+ maximumLineCount: 2
+ elide: Text.ElideRight
+ }
+ }
+ }
+}
From 2597182f3ca2d32fc8f28f92d8e175b27b546909 Mon Sep 17 00:00:00 2001
From: reze-dev <25588579+reze-dev@users.noreply.github.com>
Date: Sat, 26 Sep 2026 01:23:16 +0530
Subject: [PATCH 07/48] feat(radial): implement tactical ContextPanel with
interactive controls
---
.../desktop/surfaces/radial/ContextPanel.qml | 554 ++++++++++++++++++
1 file changed, 554 insertions(+)
create mode 100644 shell/desktop/surfaces/radial/ContextPanel.qml
diff --git a/shell/desktop/surfaces/radial/ContextPanel.qml b/shell/desktop/surfaces/radial/ContextPanel.qml
new file mode 100644
index 00000000..8896c2a8
--- /dev/null
+++ b/shell/desktop/surfaces/radial/ContextPanel.qml
@@ -0,0 +1,554 @@
+pragma ComponentBehavior: Bound
+import QtQuick
+import QtQuick.Layouts
+import "../../core"
+import "../components"
+import "../widgets"
+
+Item {
+ id: root
+
+ // =========================================================================
+ // Properties
+ // =========================================================================
+
+ property var model: null
+ property int focusedCategoryIndex: 0
+ property string selectedNodeId: ""
+ property bool isExpanded: false
+
+ readonly property var currentCategory: model ? model.getCategory(focusedCategoryIndex) : null
+ readonly property var currentNode: (model && currentCategory) ? model.getNode(focusedCategoryIndex, selectedNodeId) : null
+
+ readonly property bool isLocked: currentNode ? Boolean(currentNode.locked) : false
+ readonly property bool isActive: {
+ if (!currentNode || !model) return false;
+ var _ = model.revision;
+ return typeof currentNode.value === "function" && Boolean(currentNode.value());
+ }
+
+ width: parent ? Math.min(440, Math.max(340, parent.width * 0.3)) : 440
+ anchors.top: parent ? parent.top : undefined
+ anchors.bottom: parent ? parent.bottom : undefined
+ anchors.right: parent ? parent.right : undefined
+ anchors.topMargin: parent ? Math.max(50, parent.height * 0.06) : 60
+ anchors.bottomMargin: parent ? Math.max(20, parent.height * 0.04) : 40
+ anchors.rightMargin: parent ? Math.max(16, parent.width * 0.02) : 40
+
+ opacity: isExpanded ? 1.0 : 0.0
+ visible: opacity > 0.01
+
+ Behavior on opacity {
+ NumberAnimation { duration: Theme.durationSlow; easing.type: Easing.OutCubic }
+ }
+
+ // Slide transition
+ transform: Translate {
+ x: root.isExpanded ? 0 : 60
+ Behavior on x {
+ NumberAnimation { duration: Theme.durationSlow; easing.type: Easing.OutCubic }
+ }
+ }
+
+ // Panel Background
+ Rectangle {
+ id: bgRect
+ anchors.fill: parent
+ color: Theme.gray900
+ border.color: Theme.gray700
+ border.width: Theme.borderWidth
+ }
+
+ // Corner Brackets
+ CornerBrackets {
+ bracketColor: root.isLocked ? Theme.warningRed : Theme.acidGreen
+ }
+
+ // Inside click consumer
+ MouseArea {
+ anchors.fill: parent
+ hoverEnabled: true
+ preventStealing: true
+ onClicked: function(mouse) {
+ mouse.accepted = true;
+ }
+ }
+
+ // Main Content Column
+ ColumnLayout {
+ id: layout
+ anchors.fill: parent
+ anchors.margins: Theme.paddingXl
+ spacing: Theme.spacingLarge
+
+ // Category Breadcrumb Header
+ RowLayout {
+ Layout.fillWidth: true
+ spacing: Theme.spacingSmall
+
+ Rectangle {
+ Layout.preferredWidth: 6
+ Layout.preferredHeight: 12
+ color: root.isLocked ? Theme.warningRed : Theme.acidGreen
+ }
+
+ Text {
+ text: {
+ var catName = root.currentCategory ? root.currentCategory.name : "SYSTEM";
+ var nodeSub = root.currentNode ? root.currentNode.subtitle : "NODE.00";
+ return catName + " // " + nodeSub;
+ }
+ font.family: Theme.fontFamilyMonospace
+ font.pixelSize: Theme.fontSizeCaption
+ font.weight: Theme.fontWeightBold
+ color: root.isLocked ? Theme.warningRed : Theme.acidGreen
+ Layout.fillWidth: true
+ elide: Text.ElideRight
+ }
+ }
+
+ // Node Title
+ Text {
+ id: titleText
+ text: root.currentNode ? root.currentNode.title : "-- NO NODE SELECTED --"
+ font.family: Theme.fontFamilyMonospace
+ font.pixelSize: Theme.fontSizeTitle
+ font.weight: Theme.fontWeightBold
+ color: Theme.textPrimary
+ Layout.fillWidth: true
+ wrapMode: Text.WordWrap
+ }
+
+ // Status Pill Badge
+ Rectangle {
+ Layout.preferredHeight: 24
+ Layout.preferredWidth: statusText.implicitWidth + 16
+ radius: Theme.radiusSmall
+ color: Theme.gray800
+ border.width: 1
+ border.color: root.isLocked ? Theme.warningRed : (root.isActive ? Theme.acidGreen : Theme.gray600)
+
+ Text {
+ id: statusText
+ anchors.centerIn: parent
+ text: {
+ if (root.isLocked) return "RESTRICTED // ACCESS DENIED";
+ if (root.isActive) return "ONLINE // ACTIVE";
+ return "STANDBY";
+ }
+ font.family: Theme.fontFamilyMonospace
+ font.pixelSize: Theme.fontSizeSmall
+ font.weight: Theme.fontWeightBold
+ color: root.isLocked ? Theme.warningRed : (root.isActive ? Theme.acidGreen : Theme.textSecondary)
+ }
+ }
+
+ // Divider
+ Rectangle {
+ Layout.fillWidth: true
+ Layout.preferredHeight: Theme.dividerWidth
+ color: Theme.gray700
+ }
+
+ // Locked Prerequisite Warning Alert Banner
+ Rectangle {
+ id: lockedAlertBanner
+ Layout.fillWidth: true
+ Layout.preferredHeight: alertCol.implicitHeight + 20
+ color: Theme.gray800
+ border.color: Theme.warningRed
+ border.width: 1
+ visible: root.isLocked
+
+ RowLayout {
+ id: alertCol
+ anchors.fill: parent
+ anchors.margins: Theme.paddingMedium
+ spacing: Theme.spacingMedium
+
+ CtosIcon {
+ name: "warning"
+ size: 24
+ color: Theme.warningRed
+ Layout.alignment: Qt.AlignTop
+ }
+
+ ColumnLayout {
+ Layout.fillWidth: true
+ spacing: 2
+
+ Text {
+ text: "PREREQUISITE LOCKED"
+ font.family: Theme.fontFamilyMonospace
+ font.pixelSize: Theme.fontSizeCaption
+ font.weight: Theme.fontWeightBold
+ color: Theme.warningRed
+ }
+
+ Text {
+ text: root.currentNode ? (root.currentNode.lockReason || "Prerequisites not fulfilled.") : ""
+ font.family: Theme.fontFamilyMonospace
+ font.pixelSize: Theme.fontSizeSmall
+ color: Theme.textPrimaryDim
+ wrapMode: Text.WordWrap
+ Layout.fillWidth: true
+ }
+ }
+ }
+ }
+
+ // Description Text
+ Text {
+ id: descText
+ text: root.currentNode ? root.currentNode.description : ""
+ font.family: Theme.fontFamilyMonospace
+ font.pixelSize: Theme.fontSizeBody
+ color: Theme.textSecondary
+ wrapMode: Text.WordWrap
+ Layout.fillWidth: true
+ lineHeight: 1.3
+ }
+
+ // Interactive Settings Control Box
+ Item {
+ Layout.fillWidth: true
+ Layout.preferredHeight: 90
+ visible: root.currentNode !== null
+
+ // 1. Toggle Control
+ Item {
+ anchors.fill: parent
+ visible: root.currentNode && root.currentNode.controlType === "toggle" && !root.isLocked
+
+ RowLayout {
+ anchors.fill: parent
+ spacing: Theme.spacingLarge
+
+ ColumnLayout {
+ Layout.fillWidth: true
+ spacing: 4
+
+ Text {
+ text: "STATE TOGGLE"
+ font.family: Theme.fontFamilyMonospace
+ font.pixelSize: Theme.fontSizeCaption
+ color: Theme.textMuted
+ }
+
+ Text {
+ text: {
+ if (!root.currentNode || !root.model) return "OFF";
+ var _ = root.model.revision;
+ return (typeof root.currentNode.valueText === "function") ? root.currentNode.valueText() : "OFF";
+ }
+ font.family: Theme.fontFamilyMonospace
+ font.pixelSize: Theme.fontSizeLarge
+ font.weight: Theme.fontWeightBold
+ color: root.isActive ? Theme.acidGreen : Theme.textPrimaryDim
+ }
+ }
+
+ Rectangle {
+ id: toggleBtn
+ Layout.preferredWidth: 80
+ Layout.preferredHeight: 36
+ radius: Theme.radiusSmall
+ color: root.isActive ? Theme.acidGreen : Theme.gray800
+ border.color: root.isActive ? Theme.acidGreen : Theme.gray600
+ border.width: 1
+
+ Text {
+ anchors.centerIn: parent
+ text: root.isActive ? "ENABLED" : "DISABLED"
+ font.family: Theme.fontFamilyMonospace
+ font.pixelSize: Theme.fontSizeCaption
+ font.weight: Theme.fontWeightBold
+ color: root.isActive ? Theme.gray900 : Theme.textSecondary
+ }
+
+ MouseArea {
+ anchors.fill: parent
+ cursorShape: Qt.PointingHandCursor
+ onClicked: {
+ if (root.currentNode && typeof root.currentNode.execute === "function") {
+ root.currentNode.execute();
+ }
+ }
+ }
+ }
+ }
+ }
+
+ // 2. Slider Control
+ Item {
+ anchors.fill: parent
+ visible: root.currentNode && root.currentNode.controlType === "slider" && !root.isLocked
+
+ ColumnLayout {
+ anchors.fill: parent
+ spacing: Theme.spacingSmall
+
+ RowLayout {
+ Layout.fillWidth: true
+
+ Text {
+ text: "MAGNITUDE LEVEL"
+ font.family: Theme.fontFamilyMonospace
+ font.pixelSize: Theme.fontSizeCaption
+ color: Theme.textMuted
+ Layout.fillWidth: true
+ }
+
+ Text {
+ text: {
+ if (!root.currentNode || !root.model) return "0";
+ var _ = root.model.revision;
+ return (typeof root.currentNode.valueText === "function") ? root.currentNode.valueText() : "0";
+ }
+ font.family: Theme.fontFamilyMonospace
+ font.pixelSize: Theme.fontSizeBody
+ font.weight: Theme.fontWeightBold
+ color: Theme.acidGreen
+ }
+ }
+
+ RowLayout {
+ Layout.fillWidth: true
+ spacing: Theme.spacingMedium
+
+ // Minus step button
+ Rectangle {
+ Layout.preferredWidth: 32
+ Layout.preferredHeight: 32
+ color: Theme.gray800
+ border.color: Theme.gray600
+ border.width: 1
+
+ Text {
+ anchors.centerIn: parent
+ text: "-"
+ font.family: Theme.fontFamilyMonospace
+ font.pixelSize: Theme.fontSizeLarge
+ color: Theme.textPrimary
+ }
+
+ MouseArea {
+ anchors.fill: parent
+ cursorShape: Qt.PointingHandCursor
+ onClicked: {
+ if (!root.currentNode) return;
+ var current = (typeof root.currentNode.value === "function") ? root.currentNode.value() : 0;
+ var step = root.currentNode.step || 1;
+ var minVal = root.currentNode.minVal ?? 0;
+ var nextVal = Math.max(minVal, current - step);
+ if (typeof root.currentNode.execute === "function") {
+ root.currentNode.execute(nextVal);
+ }
+ }
+ }
+ }
+
+ // Slider Bar Visual
+ Rectangle {
+ id: sliderTrack
+ Layout.fillWidth: true
+ Layout.preferredHeight: 12
+ color: Theme.gray800
+ border.color: Theme.gray700
+ border.width: 1
+
+ Rectangle {
+ anchors.left: parent.left
+ anchors.top: parent.top
+ anchors.bottom: parent.bottom
+ width: {
+ if (!root.currentNode) return 0;
+ var _ = root.model ? root.model.revision : 0;
+ var current = (typeof root.currentNode.value === "function") ? root.currentNode.value() : 0;
+ var minVal = root.currentNode.minVal ?? 0;
+ var maxVal = root.currentNode.maxVal ?? 100;
+ var fraction = Math.max(0.0, Math.min(1.0, (current - minVal) / Math.max(1, maxVal - minVal)));
+ return parent.width * fraction;
+ }
+ color: Theme.acidGreen
+ }
+
+ MouseArea {
+ anchors.fill: parent
+ cursorShape: Qt.PointingHandCursor
+ hoverEnabled: true
+
+ function updateFromTrack(mouseX) {
+ if (!root.currentNode || typeof root.currentNode.execute !== "function") return;
+ var minVal = root.currentNode.minVal ?? 0;
+ var maxVal = root.currentNode.maxVal ?? 100;
+ var fraction = Math.max(0.0, Math.min(1.0, mouseX / sliderTrack.width));
+ var targetVal = minVal + fraction * (maxVal - minVal);
+ var step = root.currentNode.step || 1;
+ targetVal = Math.round(targetVal / step) * step;
+ root.currentNode.execute(targetVal);
+ }
+
+ onPressed: function(mouse) { updateFromTrack(mouse.x); }
+ onPositionChanged: function(mouse) { if (pressed) updateFromTrack(mouse.x); }
+ }
+ }
+
+ // Plus step button
+ Rectangle {
+ Layout.preferredWidth: 32
+ Layout.preferredHeight: 32
+ color: Theme.gray800
+ border.color: Theme.gray600
+ border.width: 1
+
+ Text {
+ anchors.centerIn: parent
+ text: "+"
+ font.family: Theme.fontFamilyMonospace
+ font.pixelSize: Theme.fontSizeLarge
+ color: Theme.textPrimary
+ }
+
+ MouseArea {
+ anchors.fill: parent
+ cursorShape: Qt.PointingHandCursor
+ onClicked: {
+ if (!root.currentNode) return;
+ var current = (typeof root.currentNode.value === "function") ? root.currentNode.value() : 0;
+ var step = root.currentNode.step || 1;
+ var maxVal = root.currentNode.maxVal ?? 100;
+ var nextVal = Math.min(maxVal, current + step);
+ if (typeof root.currentNode.execute === "function") {
+ root.currentNode.execute(nextVal);
+ }
+ }
+ }
+ }
+ }
+ }
+ }
+
+ // 3. Action Control
+ Item {
+ anchors.fill: parent
+ visible: root.currentNode && root.currentNode.controlType === "action" && !root.isLocked
+
+ RowLayout {
+ anchors.fill: parent
+ spacing: Theme.spacingLarge
+
+ Text {
+ text: "MANUAL TRIGGER"
+ font.family: Theme.fontFamilyMonospace
+ font.pixelSize: Theme.fontSizeCaption
+ color: Theme.textMuted
+ Layout.fillWidth: true
+ }
+
+ Rectangle {
+ Layout.preferredWidth: 150
+ Layout.preferredHeight: 36
+ color: Theme.gray800
+ border.color: Theme.acidGreen
+ border.width: 1
+ radius: Theme.radiusSmall
+
+ Text {
+ anchors.centerIn: parent
+ text: (root.currentNode && root.currentNode.actionLabel) ? root.currentNode.actionLabel : "EXECUTE"
+ font.family: Theme.fontFamilyMonospace
+ font.pixelSize: Theme.fontSizeCaption
+ font.weight: Theme.fontWeightBold
+ color: Theme.acidGreen
+ }
+
+ MouseArea {
+ anchors.fill: parent
+ cursorShape: Qt.PointingHandCursor
+ onClicked: {
+ if (root.currentNode && typeof root.currentNode.execute === "function") {
+ root.currentNode.execute();
+ }
+ }
+ }
+ }
+ }
+ }
+
+ // 4. Readonly Value Readout
+ Item {
+ anchors.fill: parent
+ visible: root.currentNode && (root.currentNode.controlType === "readonly" || root.isLocked)
+
+ RowLayout {
+ anchors.fill: parent
+
+ Text {
+ text: "METRIC STATE"
+ font.family: Theme.fontFamilyMonospace
+ font.pixelSize: Theme.fontSizeCaption
+ color: Theme.textMuted
+ Layout.fillWidth: true
+ }
+
+ Rectangle {
+ Layout.preferredHeight: 32
+ Layout.preferredWidth: valText.implicitWidth + 24
+ color: Theme.gray800
+ border.color: root.isLocked ? Theme.warningRed : Theme.gray600
+ border.width: 1
+
+ Text {
+ id: valText
+ anchors.centerIn: parent
+ text: {
+ if (!root.currentNode || !root.model) return "--";
+ var _ = root.model.revision;
+ return (typeof root.currentNode.valueText === "function") ? root.currentNode.valueText() : "--";
+ }
+ font.family: Theme.fontFamilyMonospace
+ font.pixelSize: Theme.fontSizeBody
+ font.weight: Theme.fontWeightBold
+ color: root.isLocked ? Theme.warningRed : Theme.textPrimary
+ }
+ }
+ }
+ }
+ }
+
+ Item {
+ Layout.fillHeight: true
+ }
+
+ // Bottom Tactical Key Navigation Hint
+ Rectangle {
+ Layout.fillWidth: true
+ Layout.preferredHeight: 30
+ color: Theme.gray800
+ border.color: Theme.gray700
+ border.width: 1
+
+ RowLayout {
+ anchors.fill: parent
+ anchors.leftMargin: Theme.paddingMedium
+ anchors.rightMargin: Theme.paddingMedium
+
+ Text {
+ text: "[ENTER] ACTIVATE [ESC] BACK [ARROWS] SELECT"
+ font.family: Theme.fontFamilyMonospace
+ font.pixelSize: Theme.fontSizeCaption
+ color: Theme.textSecondary
+ Layout.fillWidth: true
+ }
+ }
+ }
+ }
+
+ function executeCurrentNode() {
+ if (!currentNode || isLocked) return;
+ if (typeof currentNode.execute === "function") {
+ currentNode.execute();
+ }
+ }
+}
From 860b3397ace64f9eaa2738d53f3ef6215f3a68a5 Mon Sep 17 00:00:00 2001
From: reze-dev <25588579+reze-dev@users.noreply.github.com>
Date: Sat, 26 Sep 2026 01:23:21 +0530
Subject: [PATCH 08/48] feat(radial): add NavigationController for multi-modal
spatial input
---
.../surfaces/radial/NavigationController.qml | 205 ++++++++++++++++++
1 file changed, 205 insertions(+)
create mode 100644 shell/desktop/surfaces/radial/NavigationController.qml
diff --git a/shell/desktop/surfaces/radial/NavigationController.qml b/shell/desktop/surfaces/radial/NavigationController.qml
new file mode 100644
index 00000000..533a4cb9
--- /dev/null
+++ b/shell/desktop/surfaces/radial/NavigationController.qml
@@ -0,0 +1,205 @@
+pragma ComponentBehavior: Bound
+import QtQuick
+import "RadialGeometry.js" as RadialGeometry
+
+Item {
+ id: root
+
+ // =========================================================================
+ // Properties
+ // =========================================================================
+
+ property int focusedCategoryIndex: 0
+ property string selectedNodeId: ""
+ property bool isExpanded: false
+
+ property real wheelCenterX: 0
+ property real wheelCenterY: 0
+ property real wheelRotation: 0.0
+ property real surfaceWidth: 1920
+ property real surfaceHeight: 1080
+ property var activeNodes: []
+ property int categoryCount: 8
+
+ property real innerDeadZone: 45.0
+ property real outerMaxRadius: 360.0
+
+ // Signals
+ signal categoryChanged(int newIndex)
+ signal nodeChanged(string newNodeId)
+ signal expandRequested()
+ signal collapseRequested()
+ signal dismissRequested()
+ signal activateRequested()
+
+ // Spatial Mouse Movement Handler
+ function handleMouseMove(mouseX: real, mouseY: real): void {
+ if (!root.isExpanded) {
+ var dist = RadialGeometry.distance(root.wheelCenterX, root.wheelCenterY, mouseX, mouseY);
+ // Ignore if inside dead-center hub or far outside the wheel
+ if (dist < root.innerDeadZone || dist > root.outerMaxRadius) {
+ return;
+ }
+
+ var angle = RadialGeometry.angleFromCenter(root.wheelCenterX, root.wheelCenterY, mouseX, mouseY);
+ var bestIdx = RadialGeometry.findClosestSegment(angle, root.categoryCount, root.focusedCategoryIndex, 4.0);
+ if (bestIdx !== root.focusedCategoryIndex) {
+ root.focusedCategoryIndex = bestIdx;
+ root.categoryChanged(bestIdx);
+ }
+ } else {
+ // In expanded mode, constrain hover to tree zone so moving towards ContextPanel or wheel doesn't jump nodes
+ var treeMinX = root.wheelCenterX + 120.0;
+ var treeMaxX = Math.max(treeMinX + 100.0, root.surfaceWidth - 480.0);
+
+ if (mouseX < treeMinX || mouseX > treeMaxX || mouseY < 50.0 || mouseY > root.surfaceHeight - 40.0) {
+ return;
+ }
+
+ if (!root.activeNodes || root.activeNodes.length === 0) return;
+ var bestNode = RadialGeometry.findClosestNode(mouseX, mouseY, root.activeNodes, root.selectedNodeId, 25.0);
+ if (bestNode && bestNode.id && bestNode.id !== root.selectedNodeId) {
+ var d = RadialGeometry.distance(mouseX, mouseY, bestNode.screenX, bestNode.screenY);
+ if (d < 160.0) {
+ root.selectedNodeId = bestNode.id;
+ root.nodeChanged(bestNode.id);
+ }
+ }
+ }
+ }
+
+ // Mouse Wheel Handler
+ function handleWheel(angleDeltaY: int): void {
+ if (!root.isExpanded) {
+ if (angleDeltaY > 0) {
+ // Scroll up -> counter-clockwise
+ var prevIdx = (root.focusedCategoryIndex - 1 + root.categoryCount) % root.categoryCount;
+ root.focusedCategoryIndex = prevIdx;
+ root.categoryChanged(prevIdx);
+ } else if (angleDeltaY < 0) {
+ // Scroll down -> clockwise
+ var nextIdx = (root.focusedCategoryIndex + 1) % root.categoryCount;
+ root.focusedCategoryIndex = nextIdx;
+ root.categoryChanged(nextIdx);
+ }
+ } else {
+ if (!root.activeNodes || root.activeNodes.length === 0) return;
+ var curIdx = 0;
+ for (var i = 0; i < root.activeNodes.length; ++i) {
+ if (root.activeNodes[i].id === root.selectedNodeId) {
+ curIdx = i;
+ break;
+ }
+ }
+ if (angleDeltaY > 0) {
+ var pIdx = (curIdx - 1 + root.activeNodes.length) % root.activeNodes.length;
+ root.selectedNodeId = root.activeNodes[pIdx].id;
+ root.nodeChanged(root.selectedNodeId);
+ } else if (angleDeltaY < 0) {
+ var nIdx = (curIdx + 1) % root.activeNodes.length;
+ root.selectedNodeId = root.activeNodes[nIdx].id;
+ root.nodeChanged(root.selectedNodeId);
+ }
+ }
+ }
+
+ // Keyboard Key Press Handler
+ function handleKeyPress(key: int): bool {
+ // Dismissal / Back navigation
+ if (key === Qt.Key_Escape) {
+ if (root.isExpanded) {
+ root.collapseRequested();
+ return true;
+ } else {
+ root.dismissRequested();
+ return true;
+ }
+ }
+
+ if (key === Qt.Key_Backspace && root.isExpanded) {
+ root.collapseRequested();
+ return true;
+ }
+
+ // Activation / Expansion
+ if (key === Qt.Key_Return || key === Qt.Key_Enter || key === Qt.Key_Space) {
+ if (!root.isExpanded) {
+ root.expandRequested();
+ return true;
+ } else {
+ root.activateRequested();
+ return true;
+ }
+ }
+
+ // Spatial Direction Traversal
+ if (!root.isExpanded) {
+ // Root Wheel Navigation
+ if (key === Qt.Key_Up || key === Qt.Key_W) {
+ root.focusedCategoryIndex = 0; // SYSTEM (12 o'clock)
+ root.categoryChanged(0);
+ return true;
+ } else if (key === Qt.Key_Right || key === Qt.Key_D) {
+ var next = (root.focusedCategoryIndex + 1) % root.categoryCount;
+ root.focusedCategoryIndex = next;
+ root.categoryChanged(next);
+ return true;
+ } else if (key === Qt.Key_Down || key === Qt.Key_S) {
+ root.focusedCategoryIndex = 4; // AUDIO (6 o'clock)
+ root.categoryChanged(4);
+ return true;
+ } else if (key === Qt.Key_Left || key === Qt.Key_A) {
+ var prev = (root.focusedCategoryIndex - 1 + root.categoryCount) % root.categoryCount;
+ root.focusedCategoryIndex = prev;
+ root.categoryChanged(prev);
+ return true;
+ }
+ } else {
+ // Category cycling in expanded mode
+ if (key === Qt.Key_Tab || key === Qt.Key_PageDown || key === Qt.Key_BracketRight) {
+ var nextCat = (root.focusedCategoryIndex + 1) % root.categoryCount;
+ root.focusedCategoryIndex = nextCat;
+ root.categoryChanged(nextCat);
+ return true;
+ } else if (key === Qt.Key_Backtab || key === Qt.Key_PageUp || key === Qt.Key_BracketLeft) {
+ var prevCat = (root.focusedCategoryIndex - 1 + root.categoryCount) % root.categoryCount;
+ root.focusedCategoryIndex = prevCat;
+ root.categoryChanged(prevCat);
+ return true;
+ }
+
+ // Expanded Branch Graph Directional Traversal
+ var dir = -1;
+ if (key === Qt.Key_Up || key === Qt.Key_W) dir = 0;
+ else if (key === Qt.Key_Right || key === Qt.Key_D) dir = 1;
+ else if (key === Qt.Key_Down || key === Qt.Key_S) dir = 2;
+ else if (key === Qt.Key_Left || key === Qt.Key_A) dir = 3;
+
+ if (dir !== -1 && root.activeNodes && root.activeNodes.length > 0) {
+ var cur = null;
+ for (var j = 0; j < root.activeNodes.length; ++j) {
+ if (root.activeNodes[j].id === root.selectedNodeId) {
+ cur = root.activeNodes[j];
+ break;
+ }
+ }
+ if (!cur && root.activeNodes.length > 0) {
+ cur = root.activeNodes[0];
+ }
+
+ var nextNeighbor = RadialGeometry.findSpatialNeighbor(cur, root.activeNodes, dir);
+ if (nextNeighbor && nextNeighbor.id && nextNeighbor.id !== root.selectedNodeId) {
+ root.selectedNodeId = nextNeighbor.id;
+ root.nodeChanged(nextNeighbor.id);
+ return true;
+ } else if (dir === 3) {
+ // Pressing left from the root node returns to wheel
+ root.collapseRequested();
+ return true;
+ }
+ }
+ }
+
+ return false;
+ }
+}
From 036005a02bd5bcb9393d6484ce5ab00788a005a7 Mon Sep 17 00:00:00 2001
From: reze-dev <25588579+reze-dev@users.noreply.github.com>
Date: Sat, 26 Sep 2026 01:23:34 +0530
Subject: [PATCH 09/48] feat(radial): compose RadialSettings surface with HUD
telemetry and qmldir
---
.../surfaces/radial/RadialSettings.qml | 423 ++++++++++++++++++
shell/desktop/surfaces/radial/qmldir | 10 +
2 files changed, 433 insertions(+)
create mode 100644 shell/desktop/surfaces/radial/RadialSettings.qml
create mode 100644 shell/desktop/surfaces/radial/qmldir
diff --git a/shell/desktop/surfaces/radial/RadialSettings.qml b/shell/desktop/surfaces/radial/RadialSettings.qml
new file mode 100644
index 00000000..d2a7c054
--- /dev/null
+++ b/shell/desktop/surfaces/radial/RadialSettings.qml
@@ -0,0 +1,423 @@
+import QtQuick
+import QtQuick.Layouts
+import "../../core"
+import "../../services"
+import "RadialGeometry.js" as RadialGeometry
+
+FocusScope {
+ id: root
+
+ anchors.fill: parent
+ focus: true
+
+ // =========================================================================
+ // State Properties
+ // =========================================================================
+
+ property int focusedCategoryIndex: 0
+ property string selectedNodeId: "sys-core"
+ property bool isExpanded: false
+ property bool debugMode: false
+
+ property alias settingsModel: settingsModel
+ property alias navCtrl: navCtrl
+ property alias wheelMenu: wheelMenu
+
+ // Data-Driven Settings Model
+ RadialSettingsModel {
+ id: settingsModel
+ }
+
+ // Keep selectedNodeId in sync when focused category changes
+ onFocusedCategoryIndexChanged: {
+ var cat = settingsModel.getCategory(focusedCategoryIndex);
+ if (cat && cat.nodes && cat.nodes.length > 0) {
+ selectedNodeId = cat.nodes[0].id;
+ }
+ }
+
+ // Surface Focus Scope Registration
+ Component.onCompleted: {
+ OverlayController.registerFocusTarget(OverlayController.Surface.RadialSettings, root);
+ root.forceActiveFocus();
+ }
+
+ Component.onDestruction: {
+ OverlayController.unregisterFocusTarget(OverlayController.Surface.RadialSettings);
+ }
+
+ onVisibleChanged: {
+ if (visible) {
+ root.forceActiveFocus();
+ } else {
+ root.isExpanded = false;
+ }
+ }
+
+ // =========================================================================
+ // Background Scrim & Tactical Grid
+ // =========================================================================
+
+ Rectangle {
+ id: scrimBackdrop
+ anchors.fill: parent
+ color: Theme.gray900
+ opacity: 0.94
+ }
+
+ // Cybernetic Grid Background
+ Canvas {
+ id: gridCanvas
+ anchors.fill: parent
+ opacity: 0.35
+
+ onPaint: {
+ var ctx = getContext("2d");
+ ctx.clearRect(0, 0, width, height);
+ ctx.strokeStyle = Theme.gray700;
+ ctx.lineWidth = 1;
+
+ var step = 48;
+ for (var x = step; x < width; x += step) {
+ ctx.beginPath();
+ ctx.moveTo(x, 0);
+ ctx.lineTo(x, height);
+ ctx.stroke();
+ }
+ for (var y = step; y < height; y += step) {
+ ctx.beginPath();
+ ctx.moveTo(0, y);
+ ctx.lineTo(width, y);
+ ctx.stroke();
+ }
+ }
+ }
+
+ // =========================================================================
+ // Top HUD Telemetry Bar
+ // =========================================================================
+
+ Rectangle {
+ id: topHudBar
+ anchors.top: parent.top
+ anchors.left: parent.left
+ anchors.right: parent.right
+ height: 44
+ color: Theme.gray900
+ border.color: Theme.gray700
+ border.width: 1
+ z: 20
+
+ RowLayout {
+ anchors.fill: parent
+ anchors.leftMargin: Theme.paddingXl
+ anchors.rightMargin: Theme.paddingXl
+ spacing: Theme.spacingLarge
+
+ // Left System Designation
+ RowLayout {
+ spacing: Theme.spacingSmall
+
+ Rectangle {
+ Layout.preferredWidth: 8
+ Layout.preferredHeight: 14
+ color: Theme.acidGreen
+ }
+
+ Text {
+ text: "ctOS // SETTINGS MATRIX // SPEC-v4.2"
+ font.family: Theme.fontFamilyMonospace
+ font.pixelSize: Theme.fontSizeCaption
+ font.weight: Theme.fontWeightBold
+ color: Theme.acidGreen
+ }
+ }
+
+ Item {
+ Layout.fillWidth: true
+ }
+
+ // Center Status Readout
+ Text {
+ text: "SYSTEM: " + SessionService.compositorName.toUpperCase() + " // UPTIME: ONLINE"
+ font.family: Theme.fontFamilyMonospace
+ font.pixelSize: Theme.fontSizeCaption
+ color: Theme.textSecondary
+ }
+
+ Item {
+ Layout.fillWidth: true
+ }
+
+ // Right Return Badge Button
+ Rectangle {
+ Layout.preferredHeight: 28
+ Layout.preferredWidth: closeLabel.implicitWidth + 20
+ radius: Theme.radiusSmall
+ color: Theme.gray800
+ border.color: Theme.gray600
+ border.width: 1
+
+ Text {
+ id: closeLabel
+ anchors.centerIn: parent
+ text: root.isExpanded ? "[ESC] BACK TO WHEEL" : "[ESC] DISMISS MATRIX"
+ font.family: Theme.fontFamilyMonospace
+ font.pixelSize: Theme.fontSizeCaption
+ color: Theme.textPrimary
+ }
+
+ MouseArea {
+ anchors.fill: parent
+ cursorShape: Qt.PointingHandCursor
+ onClicked: {
+ if (root.isExpanded) {
+ root.isExpanded = false;
+ } else {
+ OverlayController.close();
+ }
+ }
+ }
+ }
+ }
+ }
+
+ // =========================================================================
+ // Core Visualization Components
+ // =========================================================================
+
+ // Skill Tree Graph Container (Preview Trees & Expanded Branch)
+ SkillTree {
+ id: skillTree
+ anchors.fill: parent
+ model: settingsModel
+ focusedCategoryIndex: root.focusedCategoryIndex
+ selectedNodeId: root.selectedNodeId
+ isExpanded: root.isExpanded
+ wheelCenterX: wheelMenu.wheelCenterX
+ wheelCenterY: wheelMenu.wheelCenterY
+ branchOriginX: wheelMenu.leftAnchorX + 170
+ branchOriginY: root.height / 2
+ z: 5
+
+ onNodeSelected: function(nodeId) {
+ root.selectedNodeId = nodeId;
+ }
+ }
+
+ // Circular Radial Wheel Menu
+ CircularSettingsMenu {
+ id: wheelMenu
+ anchors.fill: parent
+ model: settingsModel
+ focusedIndex: root.focusedCategoryIndex
+ isExpanded: root.isExpanded
+ leftAnchorX: 180
+ z: 10
+
+ onCategoryClicked: function(idx) {
+ if (root.focusedCategoryIndex === idx) {
+ root.isExpanded = !root.isExpanded;
+ } else {
+ root.focusedCategoryIndex = idx;
+ }
+ }
+
+ onCategoryHovered: function(idx) {
+ if (!root.isExpanded) {
+ root.focusedCategoryIndex = idx;
+ }
+ }
+ }
+
+ // Tactical Right-Side Detail Context Panel
+ ContextPanel {
+ id: contextPanel
+ model: settingsModel
+ focusedCategoryIndex: root.focusedCategoryIndex
+ selectedNodeId: root.selectedNodeId
+ isExpanded: root.isExpanded
+ z: 15
+ }
+
+ // =========================================================================
+ // Navigation Controller
+ // =========================================================================
+
+ NavigationController {
+ id: navCtrl
+ focusedCategoryIndex: root.focusedCategoryIndex
+ selectedNodeId: root.selectedNodeId
+ isExpanded: root.isExpanded
+ wheelCenterX: wheelMenu.wheelCenterX
+ wheelCenterY: wheelMenu.wheelCenterY
+ wheelRotation: wheelMenu.wheelRotation
+ surfaceWidth: root.width
+ surfaceHeight: root.height
+ activeNodes: skillTree.activeNodes
+ categoryCount: settingsModel.categoryCount
+
+ onCategoryChanged: function(idx) {
+ root.focusedCategoryIndex = idx;
+ }
+
+ onNodeChanged: function(nodeId) {
+ root.selectedNodeId = nodeId;
+ }
+
+ onExpandRequested: {
+ root.isExpanded = true;
+ }
+
+ onCollapseRequested: {
+ root.isExpanded = false;
+ }
+
+ onDismissRequested: {
+ OverlayController.close();
+ }
+
+ onActivateRequested: {
+ contextPanel.executeCurrentNode();
+ }
+ }
+
+ // Surface-Wide Mouse Hover and Wheel Dispatcher
+ MouseArea {
+ id: surfaceMouseArea
+ anchors.fill: parent
+ hoverEnabled: true
+ preventStealing: false
+ z: 1
+
+ onPositionChanged: function(mouse) {
+ navCtrl.handleMouseMove(mouse.x, mouse.y);
+ }
+
+ onWheel: function(wheel) {
+ navCtrl.handleWheel(wheel.angleDelta.y);
+ wheel.accepted = true;
+ }
+
+ onClicked: function(mouse) {
+ var distFromCenter = RadialGeometry.distance(wheelMenu.wheelCenterX, wheelMenu.wheelCenterY, mouse.x, mouse.y);
+ if (root.isExpanded) {
+ // If clicked on center hub: collapse
+ if (distFromCenter < wheelMenu.innerRadius) {
+ root.isExpanded = false;
+ } else if (distFromCenter <= wheelMenu.outerRadius + 25) {
+ // Clicked on a wheel segment in expanded mode
+ var rawAngle = RadialGeometry.angleFromCenter(wheelMenu.wheelCenterX, wheelMenu.wheelCenterY, mouse.x, mouse.y);
+ var localAngle = RadialGeometry.normalizeAngle(rawAngle - wheelMenu.wheelRotation);
+ var clickedCat = RadialGeometry.findClosestSegment(localAngle, settingsModel.categoryCount, root.focusedCategoryIndex, 0);
+ root.focusedCategoryIndex = clickedCat;
+ }
+ } else {
+ if (distFromCenter < 90) {
+ // Clicked center hub -> expand
+ root.isExpanded = true;
+ } else if (distFromCenter > 220) {
+ // Clicked far background -> dismiss
+ OverlayController.close();
+ } else {
+ // Clicked on a segment
+ var angle = RadialGeometry.angleFromCenter(wheelMenu.wheelCenterX, wheelMenu.wheelCenterY, mouse.x, mouse.y);
+ var clickedIdx = RadialGeometry.findClosestSegment(angle, settingsModel.categoryCount, root.focusedCategoryIndex, 0);
+ root.focusedCategoryIndex = clickedIdx;
+ root.isExpanded = true;
+ }
+ }
+ }
+ }
+
+ // Global Keyboard Focus Handling
+ Keys.onPressed: function(event) {
+ if (event.key === Qt.Key_F12) {
+ root.debugMode = !root.debugMode;
+ event.accepted = true;
+ return;
+ }
+
+ var handled = navCtrl.handleKeyPress(event.key);
+ if (handled) {
+ event.accepted = true;
+ }
+ }
+
+ // =========================================================================
+ // Debug Visualization Overlay (F12)
+ // =========================================================================
+
+ Item {
+ id: debugOverlay
+ anchors.fill: parent
+ visible: root.debugMode
+ z: 99
+
+ // Center crosshair lines
+ Rectangle {
+ x: wheelMenu.wheelCenterX - 50
+ y: wheelMenu.wheelCenterY
+ width: 100
+ height: 1
+ color: Theme.acidGreen
+ }
+ Rectangle {
+ x: wheelMenu.wheelCenterX
+ y: wheelMenu.wheelCenterY - 50
+ width: 1
+ height: 100
+ color: Theme.acidGreen
+ }
+
+ // Telemetry readout box
+ Rectangle {
+ anchors.bottom: parent.bottom
+ anchors.left: parent.left
+ anchors.margins: Theme.paddingLarge
+ width: 320
+ height: 120
+ color: Theme.gray900
+ border.color: Theme.acidGreen
+ border.width: 1
+
+ Column {
+ anchors.fill: parent
+ anchors.margins: Theme.paddingMedium
+ spacing: 2
+
+ Text {
+ text: "[DEBUG HUD // F12 ACTIVE]"
+ font.family: Theme.fontFamilyMonospace
+ font.pixelSize: Theme.fontSizeSmall
+ font.weight: Theme.fontWeightBold
+ color: Theme.acidGreen
+ }
+ Text {
+ text: "Category: " + root.focusedCategoryIndex + " (" + (settingsModel.getCategory(root.focusedCategoryIndex)?.name || "") + ")"
+ font.family: Theme.fontFamilyMonospace
+ font.pixelSize: Theme.fontSizeSmall
+ color: Theme.textPrimary
+ }
+ Text {
+ text: "Selected Node: " + root.selectedNodeId
+ font.family: Theme.fontFamilyMonospace
+ font.pixelSize: Theme.fontSizeSmall
+ color: Theme.textPrimary
+ }
+ Text {
+ text: "Expanded: " + root.isExpanded + " | Rotation: " + wheelMenu.wheelRotation.toFixed(1) + " deg"
+ font.family: Theme.fontFamilyMonospace
+ font.pixelSize: Theme.fontSizeSmall
+ color: Theme.textPrimary
+ }
+ Text {
+ text: "Wheel Center: (" + wheelMenu.wheelCenterX.toFixed(0) + ", " + wheelMenu.wheelCenterY.toFixed(0) + ")"
+ font.family: Theme.fontFamilyMonospace
+ font.pixelSize: Theme.fontSizeSmall
+ color: Theme.textSecondary
+ }
+ }
+ }
+ }
+}
diff --git a/shell/desktop/surfaces/radial/qmldir b/shell/desktop/surfaces/radial/qmldir
new file mode 100644
index 00000000..e693b96a
--- /dev/null
+++ b/shell/desktop/surfaces/radial/qmldir
@@ -0,0 +1,10 @@
+RadialGeometry 1.0 RadialGeometry.js
+RadialSettingsModel 1.0 RadialSettingsModel.qml
+RadialSegment 1.0 RadialSegment.qml
+SkillNode 1.0 SkillNode.qml
+SkillEdge 1.0 SkillEdge.qml
+SkillTree 1.0 SkillTree.qml
+ContextPanel 1.0 ContextPanel.qml
+NavigationController 1.0 NavigationController.qml
+CircularSettingsMenu 1.0 CircularSettingsMenu.qml
+RadialSettings 1.0 RadialSettings.qml
From 2ebcfb4ca573c74aea1c96961323933677ed3d73 Mon Sep 17 00:00:00 2001
From: reze-dev <25588579+reze-dev@users.noreply.github.com>
Date: Sat, 26 Sep 2026 01:23:42 +0530
Subject: [PATCH 10/48] feat(shell): wire RadialSettings into
OverlayController, shell.qml and ActionRegistry
---
shell/desktop/core/ActionRegistry.qml | 17 +++++++++++++++++
shell/desktop/core/OverlayController.qml | 15 ++++++++++++---
shell/shell.qml | 18 ++++++++++++++++++
3 files changed, 47 insertions(+), 3 deletions(-)
diff --git a/shell/desktop/core/ActionRegistry.qml b/shell/desktop/core/ActionRegistry.qml
index c8d917de..9c13f361 100644
--- a/shell/desktop/core/ActionRegistry.qml
+++ b/shell/desktop/core/ActionRegistry.qml
@@ -61,6 +61,23 @@ Singleton {
}
readonly property var actions: [
+ {
+ category: "Actions",
+ isApp: false,
+ id: "action-toggle-radial-settings",
+ name: "Radial Settings",
+ title: "Radial Settings",
+ description: "Open Watch Dogs inspired radial settings and skill tree matrix",
+ desc: "Open Watch Dogs inspired radial settings and skill tree matrix",
+ icon: "preferences-system",
+ enabled: true,
+ destructive: false,
+ disabledNote: "",
+ keywords: ["settings", "radial", "skill", "tree", "config", "options", "preferences", "matrix"],
+ execute: function () {
+ OverlayController.toggleRadialSettings();
+ }
+ },
{
category: "Actions",
isApp: false,
diff --git a/shell/desktop/core/OverlayController.qml b/shell/desktop/core/OverlayController.qml
index a57a7f69..bdfb7a57 100644
--- a/shell/desktop/core/OverlayController.qml
+++ b/shell/desktop/core/OverlayController.qml
@@ -11,7 +11,8 @@ Singleton {
None,
CommandDeck,
SystemRail,
- EventLog
+ EventLog,
+ RadialSettings
}
// Primary Overlay State Enum
@@ -19,7 +20,8 @@ Singleton {
None,
CommandDeck,
SystemRail,
- EventLog
+ EventLog,
+ RadialSettings
}
property var _focusTargets: ({})
@@ -33,6 +35,7 @@ Singleton {
readonly property bool isOverlayActive: activeSurface !== OverlayController.Surface.None
readonly property int surfaceCommandDeck: 1
readonly property int surfaceEventLog: 3
+ readonly property int surfaceRadialSettings: 4
// Explicit constants for zero-ambiguity access
readonly property int surfaceNone: 0
@@ -53,7 +56,7 @@ Singleton {
// Internal Transition Logic
function _isValidSurface(surface: int): bool {
- if (surface < 0 || surface > OverlayController.Surface.EventLog) {
+ if (surface < 0 || surface > OverlayController.Surface.RadialSettings) {
return false;
}
return true;
@@ -127,6 +130,9 @@ Singleton {
function openEventLog(): void {
_setSurface(OverlayController.Surface.EventLog);
}
+ function openRadialSettings(): void {
+ _setSurface(OverlayController.Surface.RadialSettings);
+ }
function openSystemRail(): void {
_setSurface(OverlayController.Surface.SystemRail);
}
@@ -173,6 +179,9 @@ Singleton {
function toggleEventLog(): void {
toggle(OverlayController.Surface.EventLog);
}
+ function toggleRadialSettings(): void {
+ toggle(OverlayController.Surface.RadialSettings);
+ }
function toggleSystemRail(): void {
toggle(OverlayController.Surface.SystemRail);
}
diff --git a/shell/shell.qml b/shell/shell.qml
index e3432e53..e643295e 100644
--- a/shell/shell.qml
+++ b/shell/shell.qml
@@ -130,6 +130,14 @@ Scope {
function toggleNetwork(): void {
root.toggleNetwork(null);
}
+
+ function toggleRadialSettings(): void {
+ OverlayController.toggleRadialSettings();
+ }
+
+ function openRadialSettings(): void {
+ OverlayController.openRadialSettings();
+ }
}
function resolveTargetScreen(): var {
@@ -623,6 +631,16 @@ Scope {
source: "desktop/surfaces/EventLog.qml"
visible: OverlayController.activeSurface === OverlayController.Surface.EventLog
}
+
+ Loader {
+ id: radialSettingsLoader
+
+ anchors.fill: parent
+ asynchronous: false
+ active: true
+ visible: OverlayController.activeSurface === OverlayController.Surface.RadialSettings
+ source: "desktop/surfaces/radial/RadialSettings.qml"
+ }
}
}
From 0895e3a8ae8213b6fb34bf15ba032d42c562ac90 Mon Sep 17 00:00:00 2001
From: reze-dev <25588579+reze-dev@users.noreply.github.com>
Date: Sat, 26 Sep 2026 01:23:47 +0530
Subject: [PATCH 11/48] test(radial): add static contract audit and Quickshell
runtime harness
---
.../harness/test_radial_settings_harness.qml | 210 +++++++++++++
tests/e2e/test_radial_settings_audit.py | 279 ++++++++++++++++++
2 files changed, 489 insertions(+)
create mode 100644 tests/e2e/harness/test_radial_settings_harness.qml
create mode 100644 tests/e2e/test_radial_settings_audit.py
diff --git a/tests/e2e/harness/test_radial_settings_harness.qml b/tests/e2e/harness/test_radial_settings_harness.qml
new file mode 100644
index 00000000..46d34acc
--- /dev/null
+++ b/tests/e2e/harness/test_radial_settings_harness.qml
@@ -0,0 +1,210 @@
+pragma ComponentBehavior: Bound
+
+import QtQuick
+import Quickshell
+import Quickshell.Io
+import desktop.core
+import desktop.services
+import desktop.surfaces.radial
+
+Scope {
+ id: root
+
+ property int passCount: 0
+ property int failCount: 0
+ property int step: 0
+
+ function assertCondition(testId, desc, condition, details) {
+ if (condition) {
+ passCount++;
+ console.log("[PASS] " + testId + ": " + desc + (details ? " (" + details + ")" : ""));
+ } else {
+ failCount++;
+ console.error("[FAIL] " + testId + ": " + desc + (details ? " (" + details + ")" : ""));
+ }
+ }
+
+ Item {
+ id: testContainer
+ width: 1920
+ height: 1080
+
+ Loader {
+ id: radialLoader
+ anchors.fill: parent
+ active: true
+ asynchronous: false
+ source: "file://" + (Quickshell.env("PROJECT_ROOT") || "/home/reze/Projects/ctOS") + "/shell/desktop/surfaces/radial/RadialSettings.qml"
+ }
+ }
+
+ property bool origFlowVisible: false
+
+ Timer {
+ id: testRunner
+ interval: 50
+ repeat: true
+ running: true
+
+ onTriggered: {
+ try {
+ var radial = radialLoader.item;
+
+ switch (root.step) {
+ case 0:
+ assertCondition("RADIAL.RUN.01", "RadialSettings component loaded successfully",
+ radial !== null && radialLoader.status === Loader.Ready);
+ root.step = 1;
+ break;
+
+ case 1:
+ assertCondition("RADIAL.RUN.02", "Default focused category is SYSTEM (0)",
+ radial.focusedCategoryIndex === 0,
+ "focusedCategoryIndex=" + radial.focusedCategoryIndex);
+
+ assertCondition("RADIAL.RUN.03", "Default selected node is sys-core",
+ radial.selectedNodeId === "sys-core",
+ "selectedNodeId=" + radial.selectedNodeId);
+
+ assertCondition("RADIAL.RUN.04", "Default mode is ROOT_MENU (not expanded)",
+ radial.isExpanded === false);
+
+ root.step = 2;
+ break;
+
+ case 2:
+ // Change category to NETWORK (index 3)
+ radial.focusedCategoryIndex = 3;
+
+ assertCondition("RADIAL.RUN.05", "Category switched to NETWORK (index 3)",
+ radial.focusedCategoryIndex === 3);
+
+ assertCondition("RADIAL.RUN.06", "Selected node updated to net-core on category switch",
+ radial.selectedNodeId === "net-core",
+ "selectedNodeId=" + radial.selectedNodeId);
+
+ root.step = 3;
+ break;
+
+ case 3:
+ // Expand category
+ radial.isExpanded = true;
+
+ assertCondition("RADIAL.RUN.07", "isExpanded transitions to true",
+ radial.isExpanded === true);
+
+ root.step = 4;
+ break;
+
+ case 4:
+ // Select child node net-flow
+ radial.selectedNodeId = "net-flow";
+
+ assertCondition("RADIAL.RUN.08", "Child node net-flow selected",
+ radial.selectedNodeId === "net-flow");
+
+ root.origFlowVisible = Settings.widgetNetworkFlowVisible;
+ root.step = 5;
+ break;
+
+ case 5:
+ // Test interactive toggle mutation
+ Settings.widgetNetworkFlowVisible = !root.origFlowVisible;
+ assertCondition("RADIAL.RUN.09", "Settings state mutation verified",
+ Settings.widgetNetworkFlowVisible === !root.origFlowVisible);
+
+ // Restore original value
+ Settings.widgetNetworkFlowVisible = root.origFlowVisible;
+ Settings.save();
+
+ root.step = 6;
+ break;
+
+ case 6:
+ // Test Slider NaN immunity: execute app-bar-height with no arguments
+ var origBarHeight = Settings.barHeight;
+ var appCat = radial.settingsModel.getCategory(1); // APPEARANCE
+ var barNode = radial.settingsModel.getNode(1, "app-bar-height");
+ assertCondition("RADIAL.SLIDER.01", "app-bar-height node retrieved", barNode !== null);
+
+ // Call execute() with no args (simulating Enter key)
+ barNode.execute();
+ assertCondition("RADIAL.SLIDER.02", "barHeight remains a valid finite number and NOT NaN",
+ typeof Settings.barHeight === "number" && !isNaN(Settings.barHeight) &&
+ Settings.barHeight >= 28 && Settings.barHeight <= 48,
+ "barHeight=" + Settings.barHeight);
+
+ // Restore original bar height
+ Settings.barHeight = origBarHeight;
+ Settings.save();
+
+ root.step = 7;
+ break;
+
+ case 7:
+ // Test Mouse boundary guard in expanded mode:
+ // When cursor is inside ContextPanel area (x=1600, y=400), tree node selection must NOT change!
+ radial.selectedNodeId = "net-core";
+ radial.navCtrl.handleMouseMove(1600, 400);
+
+ assertCondition("RADIAL.HOVER.01", "Cursor inside ContextPanel zone does not hijack selected node",
+ radial.selectedNodeId === "net-core",
+ "selectedNodeId=" + radial.selectedNodeId);
+
+ root.step = 8;
+ break;
+
+ case 8:
+ // Test Keyboard category cycling in expanded mode (Tab advances category)
+ var prevCatIdx = radial.focusedCategoryIndex;
+ var handledTab = radial.navCtrl.handleKeyPress(Qt.Key_Tab);
+ assertCondition("RADIAL.KEY.01", "Tab key advances category in expanded mode",
+ handledTab && radial.focusedCategoryIndex === (prevCatIdx + 1) % 8,
+ "focusedCategoryIndex=" + radial.focusedCategoryIndex);
+
+ root.step = 9;
+ break;
+
+ case 9:
+ // Test Keyboard Left arrow on root node collapses to wheel menu
+ var curCat = radial.settingsModel.getCategory(radial.focusedCategoryIndex);
+ radial.selectedNodeId = curCat.nodes[0].id;
+ var handledLeft = radial.navCtrl.handleKeyPress(Qt.Key_Left);
+
+ assertCondition("RADIAL.KEY.02", "Left arrow at root node requests collapse",
+ handledLeft);
+
+ root.step = 10;
+ break;
+
+ case 10:
+ // Collapse back to ROOT_MENU
+ radial.isExpanded = false;
+
+ assertCondition("RADIAL.RUN.10", "isExpanded collapses cleanly to false",
+ radial.isExpanded === false);
+
+ root.step = 11;
+ break;
+
+ case 11:
+ testRunner.running = false;
+ console.log("================================================================");
+ console.log("RADIAL SETTINGS RUNTIME RESULTS: Passed=" + root.passCount + ", Failed=" + root.failCount);
+ if (root.failCount === 0 && root.passCount >= 15) {
+ console.log("=== PASS: ALL RADIAL SETTINGS RUNTIME CHECKS SUCCESSFUL ===");
+ } else {
+ console.error("=== FAIL: RADIAL SETTINGS RUNTIME HARNESS FAILED ===");
+ }
+ console.log("================================================================");
+ Qt.quit();
+ break;
+ }
+ } catch (err) {
+ console.error("ASSERTION_FAILED: Exception in runtime step " + root.step + ": " + err);
+ testRunner.running = false;
+ Qt.quit();
+ }
+ }
+ }
+}
diff --git a/tests/e2e/test_radial_settings_audit.py b/tests/e2e/test_radial_settings_audit.py
new file mode 100644
index 00000000..4d34d72b
--- /dev/null
+++ b/tests/e2e/test_radial_settings_audit.py
@@ -0,0 +1,279 @@
+#!/usr/bin/env python3
+"""
+test_radial_settings_audit.py - Comprehensive Static & Contract Verification Suite
+for ctOS Radial Settings / Skill-Tree UI.
+
+Verification Coverage:
+1. Component Existence & Module Registration (qmldir)
+2. Zero Hardcoded Hex Colors (Strict Theme Token Policy)
+3. 100% Monospace Typography Purity (Theme.fontFamilyMonospace)
+4. OverlayController Integration (Surface.RadialSettings, methods, boundaries)
+5. Shell Integration (IPC handlers, Loader in surfaceContainer)
+6. ActionRegistry Integration (action-toggle-radial-settings)
+7. Settings Data Model Structure (8 categories, real bindings, child nodes)
+8. RadialGeometry Mathematical Verification
+9. Static QML Linter Audit (qmllint on all radial components)
+"""
+
+import os
+import re
+import sys
+import math
+import subprocess
+
+PROJECT_ROOT = os.path.abspath(os.path.join(os.path.dirname(__file__), "../.."))
+RADIAL_DIR = os.path.join(PROJECT_ROOT, "shell/desktop/surfaces/radial")
+
+PASS_COUNT = 0
+FAIL_COUNT = 0
+
+def audit_assert(check_id, desc, condition, details=""):
+ global PASS_COUNT, FAIL_COUNT
+ if condition:
+ PASS_COUNT += 1
+ print(f"[PASS] {check_id}: {desc} ({details})")
+ else:
+ FAIL_COUNT += 1
+ print(f"[FAIL] {check_id}: {desc} ({details})", file=sys.stderr)
+
+print("=" * 70)
+print("STATIC & CONTRACT AUDIT: CTOS RADIAL SETTINGS / SKILL TREE UI")
+print("=" * 70)
+
+# ==============================================================================
+# 1. COMPONENT EXISTENCE & QMLDIR REGISTRATION
+# ==============================================================================
+expected_components = [
+ "RadialGeometry.js",
+ "RadialSettingsModel.qml",
+ "RadialSegment.qml",
+ "SkillNode.qml",
+ "SkillEdge.qml",
+ "SkillTree.qml",
+ "ContextPanel.qml",
+ "NavigationController.qml",
+ "CircularSettingsMenu.qml",
+ "RadialSettings.qml",
+ "qmldir"
+]
+
+for comp in expected_components:
+ path = os.path.join(RADIAL_DIR, comp)
+ audit_assert(f"FILE-{comp}", f"File exists: {comp}", os.path.isfile(path), path)
+
+qmldir_path = os.path.join(RADIAL_DIR, "qmldir")
+with open(qmldir_path, "r", encoding="utf-8") as f:
+ qmldir_content = f.read()
+
+for comp in expected_components:
+ if comp == "qmldir":
+ continue
+ cname = comp.split(".")[0]
+ audit_assert(f"QMLDIR-{cname}", f"{cname} declared in qmldir", f"{cname} 1.0 {comp}" in qmldir_content)
+
+# ==============================================================================
+# 2. ZERO HARDCODED HEX COLORS
+# ==============================================================================
+hex_pattern = re.compile(r'#[0-9a-fA-F]{3,8}')
+qml_files = [f for f in os.listdir(RADIAL_DIR) if f.endswith(".qml")]
+
+total_hex_matches = 0
+for qml in qml_files:
+ path = os.path.join(RADIAL_DIR, qml)
+ with open(path, "r", encoding="utf-8") as f:
+ lines = f.readlines()
+ matches = []
+ for idx, line in enumerate(lines, 1):
+ found = hex_pattern.findall(line)
+ if found:
+ matches.append((idx, line.strip(), found))
+ if matches:
+ total_hex_matches += len(matches)
+ print(f"Hardcoded hex in {qml}: {matches}", file=sys.stderr)
+ audit_assert(f"HEX-{qml}", f"Zero hardcoded hex colors in {qml}", len(matches) == 0, f"found {len(matches)}")
+
+audit_assert("ZERO-HEX-ALL", "Total hardcoded hex colors across all radial components is 0", total_hex_matches == 0)
+
+# ==============================================================================
+# 3. 100% MONOSPACE TYPOGRAPHY PURITY
+# ==============================================================================
+text_pattern = re.compile(r'\bText\s*\{')
+total_text_tags = 0
+total_font_mono_tags = 0
+
+for qml in qml_files:
+ path = os.path.join(RADIAL_DIR, qml)
+ with open(path, "r", encoding="utf-8") as f:
+ content = f.read()
+
+ # Count Text blocks
+ texts = text_pattern.findall(content)
+ total_text_tags += len(texts)
+
+ # In every Text block, verify font.family uses Theme.fontFamilyMonospace
+ mono_matches = re.findall(r'font\.family:\s*Theme\.fontFamilyMonospace', content)
+ total_font_mono_tags += len(mono_matches)
+
+audit_assert("MONOSPACE-PURITY", "All Text elements enforce Theme.fontFamilyMonospace",
+ total_text_tags > 0 and total_text_tags == total_font_mono_tags,
+ f"Text tags: {total_text_tags}, Monospace tags: {total_font_mono_tags}")
+
+# ==============================================================================
+# 4. OVERLAY CONTROLLER INTEGRATION
+# ==============================================================================
+oc_path = os.path.join(PROJECT_ROOT, "shell/desktop/core/OverlayController.qml")
+with open(oc_path, "r", encoding="utf-8") as f:
+ oc_content = f.read()
+
+audit_assert("OC-SURFACE-ENUM", "OverlayController.Surface includes RadialSettings",
+ "RadialSettings" in oc_content)
+audit_assert("OC-OVERLAY-ENUM", "OverlayController.OverlayType includes RadialSettings",
+ "RadialSettings" in oc_content)
+audit_assert("OC-CONST", "surfaceRadialSettings constant defined as 4",
+ "readonly property int surfaceRadialSettings: 4" in oc_content)
+audit_assert("OC-OPEN-METHOD", "openRadialSettings() method defined",
+ "function openRadialSettings(): void" in oc_content)
+audit_assert("OC-TOGGLE-METHOD", "toggleRadialSettings() method defined",
+ "function toggleRadialSettings(): void" in oc_content)
+audit_assert("OC-BOUNDARY", "_isValidSurface bounds to RadialSettings",
+ "surface > OverlayController.Surface.RadialSettings" in oc_content)
+
+# ==============================================================================
+# 5. SHELL INTEGRATION
+# ==============================================================================
+shell_path = os.path.join(PROJECT_ROOT, "shell/shell.qml")
+with open(shell_path, "r", encoding="utf-8") as f:
+ shell_content = f.read()
+
+audit_assert("SHELL-IPC-TOGGLE", "IPC handler includes toggleRadialSettings",
+ "function toggleRadialSettings(): void" in shell_content)
+audit_assert("SHELL-IPC-OPEN", "IPC handler includes openRadialSettings",
+ "function openRadialSettings(): void" in shell_content)
+audit_assert("SHELL-LOADER", "overlayHost has radialSettingsLoader",
+ "id: radialSettingsLoader" in shell_content and
+ "desktop/surfaces/radial/RadialSettings.qml" in shell_content and
+ "OverlayController.Surface.RadialSettings" in shell_content)
+
+# ==============================================================================
+# 6. ACTION REGISTRY INTEGRATION
+# ==============================================================================
+ar_path = os.path.join(PROJECT_ROOT, "shell/desktop/core/ActionRegistry.qml")
+with open(ar_path, "r", encoding="utf-8") as f:
+ ar_content = f.read()
+
+audit_assert("ACTION-REG-ID", "action-toggle-radial-settings registered in ActionRegistry",
+ '"action-toggle-radial-settings"' in ar_content)
+audit_assert("ACTION-REG-CALL", "Action calls OverlayController.toggleRadialSettings()",
+ "OverlayController.toggleRadialSettings();" in ar_content)
+
+# ==============================================================================
+# 7. DATA MODEL 8 CATEGORIES CONTRACT
+# ==============================================================================
+model_path = os.path.join(RADIAL_DIR, "RadialSettingsModel.qml")
+with open(model_path, "r", encoding="utf-8") as f:
+ model_content = f.read()
+
+categories_expected = [
+ ("system", "SYSTEM"),
+ ("appearance", "APPEARANCE"),
+ ("desktop", "DESKTOP"),
+ ("network", "NETWORK"),
+ ("audio", "AUDIO"),
+ ("input", "INPUT"),
+ ("power", "POWER"),
+ ("security", "SECURITY")
+]
+
+for cat_id, cat_name in categories_expected:
+ audit_assert(f"CAT-{cat_id}", f"Category {cat_name} defined in model",
+ f'id: "{cat_id}"' in model_content and f'name: "{cat_name}"' in model_content)
+
+# Verify real ctOS service bindings exist in model
+audit_assert("MODEL-SETTINGS-BIND", "Model binds to Settings", "Settings." in model_content)
+audit_assert("MODEL-AUDIO-BIND", "Model binds to AudioService", "AudioService." in model_content)
+audit_assert("MODEL-NET-BIND", "Model binds to NetworkService", "NetworkService." in model_content)
+audit_assert("MODEL-PWR-BIND", "Model binds to PowerService", "PowerService." in model_content)
+audit_assert("MODEL-SESSION-BIND", "Model binds to SessionService", "SessionService." in model_content)
+
+# ==============================================================================
+# 8. RADIAL GEOMETRY MATHEMATICAL ALGORITHMS
+# ==============================================================================
+# Python implementation testing the mathematical properties of RadialGeometry algorithms
+def deg_to_rad(d): return (d * math.pi) / 180.0
+def rad_to_deg(r): return (r * 180.0) / math.pi
+def normalize_angle(degrees):
+ a = degrees % 360
+ if a > 180: a -= 360
+ if a < -180: a += 360
+ return a
+def angle_diff(f, t): return normalize_angle(t - f)
+def point_on_circle(cx, cy, r, angle_deg):
+ rad = deg_to_rad(angle_deg)
+ return (cx + r * math.cos(rad), cy + r * math.sin(rad))
+
+# Test normalize_angle
+audit_assert("GEO-NORM-1", "normalize_angle(0) == 0", normalize_angle(0) == 0)
+audit_assert("GEO-NORM-2", "normalize_angle(360) == 0", normalize_angle(360) == 0)
+audit_assert("GEO-NORM-3", "normalize_angle(270) == -90", normalize_angle(270) == -90)
+audit_assert("GEO-NORM-4", "normalize_angle(-270) == 90", normalize_angle(-270) == 90)
+
+# Test angle_diff (shortest path without 360 spin)
+audit_assert("GEO-DIFF-1", "angle_diff(170, -170) == 20", angle_diff(170, -170) == 20)
+audit_assert("GEO-DIFF-2", "angle_diff(-170, 170) == -20", angle_diff(-170, 170) == -20)
+audit_assert("GEO-DIFF-3", "angle_diff(0, 90) == 90", angle_diff(0, 90) == 90)
+audit_assert("GEO-DIFF-4", "angle_diff(90, 0) == -90", angle_diff(90, 0) == -90)
+audit_assert("GEO-DIFF-5", "angle_diff(-180, 135) == -45 (POWER to SECURITY boundary)", angle_diff(-180, 135) == -45)
+audit_assert("GEO-DIFF-6", "angle_diff(135, -180) == 45 (SECURITY to POWER boundary)", angle_diff(135, -180) == 45)
+
+# Test point_on_circle
+pt_0 = point_on_circle(100, 100, 50, 0)
+audit_assert("GEO-PT-0", "0 deg point is at (cx + r, cy)", abs(pt_0[0] - 150) < 1e-5 and abs(pt_0[1] - 100) < 1e-5)
+pt_90 = point_on_circle(100, 100, 50, 90)
+audit_assert("GEO-PT-90", "90 deg point is at (cx, cy + r)", abs(pt_90[0] - 100) < 1e-5 and abs(pt_90[1] - 150) < 1e-5)
+
+# Verification of defensive guards and interaction robustness
+audit_assert("MODEL-SLIDER-GUARD", "Model contains isNaN guard against slider NaN corruption", "isNaN(val)" in model_content)
+
+nav_path = os.path.join(RADIAL_DIR, "NavigationController.qml")
+with open(nav_path, "r", encoding="utf-8") as f:
+ nav_content = f.read()
+audit_assert("NAV-BOUNDS", "NavigationController constrains expanded hover to tree zone", "treeMinX" in nav_content)
+audit_assert("NAV-CYCLE", "NavigationController supports Tab category cycling", "Qt.Key_Tab" in nav_content)
+
+panel_path = os.path.join(RADIAL_DIR, "ContextPanel.qml")
+with open(panel_path, "r", encoding="utf-8") as f:
+ panel_content = f.read()
+audit_assert("PANEL-SLIDER-SCRUB", "ContextPanel supports interactive mouse scrubbing on slider track", "updateFromTrack" in panel_content)
+
+
+# ==============================================================================
+# 9. STATIC QML LINTER AUDIT
+# ==============================================================================
+qmllint_bin = "/nix/store/10553j4116y6jllliqpg5kz7d35bblab-qtdeclarative-6.11.2/bin/qmllint"
+if not os.path.isfile(qmllint_bin):
+ import shutil
+ qmllint_bin = shutil.which("qmllint") or qmllint_bin
+
+qt_qml_dir = "/nix/store/10553j4116y6jllliqpg5kz7d35bblab-qtdeclarative-6.11.2/lib/qt-6/qml"
+
+for qml in sorted(qml_files):
+ target = os.path.join(RADIAL_DIR, qml)
+ cmd = [
+ qmllint_bin,
+ "-I", qt_qml_dir,
+ "-I", os.path.join(PROJECT_ROOT, "shell/desktop/core"),
+ "-I", os.path.join(PROJECT_ROOT, "shell/desktop/services"),
+ "-I", os.path.join(PROJECT_ROOT, "shell/desktop/surfaces/components"),
+ "-I", os.path.join(PROJECT_ROOT, "shell/desktop/surfaces/widgets"),
+ "-I", RADIAL_DIR,
+ target
+ ]
+ proc = subprocess.run(cmd, capture_output=True, text=True)
+ audit_assert(f"QMLLINT-{qml}", f"qmllint check for {qml}", proc.returncode == 0,
+ f"ret={proc.returncode}")
+
+print("=" * 70)
+print(f"AUDIT SUMMARY: {PASS_COUNT} PASSED, {FAIL_COUNT} FAILED")
+print("=" * 70)
+
+sys.exit(0 if FAIL_COUNT == 0 else 1)
From 79241e466a3992d85c591fa4b21b2f558fbd9df2 Mon Sep 17 00:00:00 2001
From: reze-dev <25588579+reze-dev@users.noreply.github.com>
Date: Sat, 26 Sep 2026 05:41:53 +0530
Subject: [PATCH 12/48] fix(radial): apply blueprint fixes, replace
ColorOverlay with MultiEffect mask
---
.../desktop/surfaces/components/CtosIcon.qml | 26 +-
.../surfaces/radial/CircularSettingsMenu.qml | 17 +-
.../desktop/surfaces/radial/RadialSegment.qml | 12 -
.../surfaces/radial/RadialSettingsModel.qml | 222 ++++++++++++------
shell/desktop/surfaces/radial/SkillNode.qml | 17 --
shell/desktop/surfaces/radial/SkillTree.qml | 71 +++---
6 files changed, 220 insertions(+), 145 deletions(-)
diff --git a/shell/desktop/surfaces/components/CtosIcon.qml b/shell/desktop/surfaces/components/CtosIcon.qml
index 22b7463d..3f960251 100644
--- a/shell/desktop/surfaces/components/CtosIcon.qml
+++ b/shell/desktop/surfaces/components/CtosIcon.qml
@@ -130,12 +130,19 @@ Item {
visible: false
}
+ Rectangle {
+ id: systemColorRect
+ anchors.fill: systemSvg
+ color: root.effectiveColor
+ visible: false
+ }
+
MultiEffect {
id: systemEffect
anchors.fill: systemSvg
- source: systemSvg
- colorization: 1.0
- colorizationColor: root.effectiveColor
+ source: systemColorRect
+ maskEnabled: true
+ maskSource: systemSvg
visible: root.isSystemIcon && (systemSvg.status !== Image.Error && systemSvg.status !== Image.Null)
}
}
@@ -170,12 +177,19 @@ Item {
visible: false
}
+ Rectangle {
+ id: fallbackColorRect
+ anchors.fill: fallbackRawImage
+ color: root.effectiveColor
+ visible: false
+ }
+
MultiEffect {
id: fallbackEffect
anchors.fill: fallbackRawImage
- source: fallbackRawImage
- colorization: 1.0
- colorizationColor: root.effectiveColor
+ source: fallbackColorRect
+ maskEnabled: true
+ maskSource: fallbackRawImage
visible: root.isShaderFallback
}
}
diff --git a/shell/desktop/surfaces/radial/CircularSettingsMenu.qml b/shell/desktop/surfaces/radial/CircularSettingsMenu.qml
index e76b4ced..b3214536 100644
--- a/shell/desktop/surfaces/radial/CircularSettingsMenu.qml
+++ b/shell/desktop/surfaces/radial/CircularSettingsMenu.qml
@@ -14,10 +14,10 @@ Item {
property int focusedIndex: 0
property bool isExpanded: false
- property real baseDiameter: 360
- property real innerRadius: 95
- property real outerRadius: 170
- property real leftAnchorX: 180
+ property real baseDiameter: 420
+ property real innerRadius: 110
+ property real outerRadius: 205
+ property real leftAnchorX: Math.max(100, width * 0.06)
// Animated Wheel Center Coordinates
property real targetCenterX: isExpanded ? leftAnchorX : (width / 2)
@@ -32,7 +32,7 @@ Item {
}
// Target Rotation: Aligns selected category to 0 deg (pointing right)
- readonly property real selectedBaseAngle: -90.0 + focusedIndex * 45.0
+ readonly property real selectedBaseAngle: -90.0 + focusedIndex * (360.0 / (root.model ? root.model.categoryCount : 8))
readonly property real desiredAngle: isExpanded ? RadialGeometry.normalizeAngle(-selectedBaseAngle) : 0.0
property real targetWheelRotation: desiredAngle
property real wheelRotation: targetWheelRotation
@@ -65,7 +65,7 @@ Item {
// 8 Annular Sectors
Repeater {
- model: 8
+ model: root.model ? root.model.categoryCount : 0
delegate: RadialSegment {
id: segItem
width: wheelContainer.width
@@ -78,8 +78,9 @@ Item {
cy: wheelContainer.height / 2
baseInnerRadius: root.innerRadius
baseOuterRadius: root.outerRadius
- startAngle: -90.0 + index * 45.0 - 21.5
- endAngle: -90.0 + index * 45.0 + 21.5
+ readonly property real segAngle: 360.0 / (root.model ? root.model.categoryCount : 8)
+ startAngle: -90.0 + index * segAngle - (segAngle / 2.0 - 1.0)
+ endAngle: -90.0 + index * segAngle + (segAngle / 2.0 - 1.0)
wheelRotation: root.wheelRotation
isFocused: root.focusedIndex === index && !root.isExpanded
isSelected: root.focusedIndex === index && root.isExpanded
diff --git a/shell/desktop/surfaces/radial/RadialSegment.qml b/shell/desktop/surfaces/radial/RadialSegment.qml
index 26f76766..cbaac69f 100644
--- a/shell/desktop/surfaces/radial/RadialSegment.qml
+++ b/shell/desktop/surfaces/radial/RadialSegment.qml
@@ -126,17 +126,5 @@ Item {
}
}
- MouseArea {
- anchors.fill: parent
- cursorShape: Qt.PointingHandCursor
- hoverEnabled: true
-
- onEntered: {
- root.hovered();
- }
- onClicked: {
- root.clicked();
- }
- }
}
}
diff --git a/shell/desktop/surfaces/radial/RadialSettingsModel.qml b/shell/desktop/surfaces/radial/RadialSettingsModel.qml
index a806f05d..b0be47b7 100644
--- a/shell/desktop/surfaces/radial/RadialSettingsModel.qml
+++ b/shell/desktop/surfaces/radial/RadialSettingsModel.qml
@@ -30,8 +30,6 @@ Item {
}
}
- readonly property int categoryCount: categories.length
-
// 8 Categories arranged clockwise starting at 12 o'clock (-90 deg):
// 0: SYSTEM (-90 deg)
// 1: APPEARANCE (-45 deg)
@@ -49,19 +47,19 @@ Item {
id: "system",
name: "SYSTEM",
subtitle: "KERNEL TELEMETRY // SCHEDULER",
- icon: "gear",
+ icon: "battery",
description: "Hardware telemetry monitors, procfs kernel scheduler, and CPU thread profiling matrix.",
nodes: [
{
id: "sys-core",
title: "KERNEL CORE",
subtitle: "SYS.01 // SCHEDULER",
- icon: "gear",
+ icon: "wifi",
description: "ctOS Linux kernel telemetry subsystem and process scheduler core.",
locked: false,
lockReason: "",
requires: [],
- pos: { x: 70, y: 0 },
+ pos: { x: 98, y: -25 },
edges: ["sys-cpu-hex", "sys-ram-bar"],
controlType: "readonly",
value: function() { return SystemMonitorService.available ? 1 : 0; },
@@ -72,12 +70,12 @@ Item {
id: "sys-cpu-hex",
title: "CPU HEX-GRID",
subtitle: "SYS.02 // CORES",
- icon: "gear",
+ icon: "lock",
description: "Per-core CPU utilization hex matrix in the ambient desktop status bar.",
locked: false,
lockReason: "",
requires: ["sys-core"],
- pos: { x: 230, y: -100 },
+ pos: { x: 238, y: -130 },
edges: ["sys-profiler"],
controlType: "toggle",
value: function() { return Settings.widgetCpuHexGridVisible; },
@@ -92,12 +90,12 @@ Item {
id: "sys-ram-bar",
title: "RAM BLOCK-BAR",
subtitle: "SYS.03 // MEMORY",
- icon: "gear",
+ icon: "warning",
description: "Physical memory and swap page allocation telemetry bar widget.",
locked: false,
lockReason: "",
requires: ["sys-core"],
- pos: { x: 230, y: 100 },
+ pos: { x: 260, y: 97 },
edges: [],
controlType: "toggle",
value: function() { return Settings.widgetRamBlockBarVisible; },
@@ -112,12 +110,12 @@ Item {
id: "sys-profiler",
title: "TARGET PROFILER",
subtitle: "SYS.04 // THREADS",
- icon: "gear",
+ icon: "warning",
description: "Active system process profiler and execution latency inspector.",
locked: false,
lockReason: "",
requires: ["sys-cpu-hex"],
- pos: { x: 390, y: -100 },
+ pos: { x: 430, y: -61 },
edges: [],
controlType: "toggle",
value: function() { return Settings.widgetTargetProfilerVisible; },
@@ -145,12 +143,12 @@ Item {
id: "app-engine",
title: "DISPLAY ENGINE",
subtitle: "APP.01 // THEME",
- icon: "brightness",
+ icon: "battery",
description: "ctOS visual styling pipeline and industrial cyberpunk theme framework.",
locked: false,
lockReason: "",
requires: [],
- pos: { x: 70, y: 0 },
+ pos: { x: 76, y: 33 },
edges: ["app-motion", "app-bar-height"],
controlType: "readonly",
value: function() { return 1; },
@@ -161,12 +159,12 @@ Item {
id: "app-motion",
title: "TACTICAL MOTION",
subtitle: "APP.02 // KINETICS",
- icon: "brightness",
+ icon: "microphone",
description: "High-speed cybernetic UI animations, transitions, and kinetic physics.",
locked: false,
lockReason: "",
requires: ["app-engine"],
- pos: { x: 230, y: -100 },
+ pos: { x: 214, y: -132 },
edges: ["app-palette"],
controlType: "toggle",
value: function() { return !Settings.reducedMotion; },
@@ -181,12 +179,12 @@ Item {
id: "app-bar-height",
title: "BAR HEIGHT",
subtitle: "APP.03 // GEOMETRY",
- icon: "brightness",
+ icon: "battery",
description: "Vertical pixel thickness of the top ambient system telemetry bar.",
locked: false,
lockReason: "",
requires: ["app-engine"],
- pos: { x: 230, y: 100 },
+ pos: { x: 195, y: 89 },
edges: [],
controlType: "slider",
minVal: 28,
@@ -208,12 +206,12 @@ Item {
id: "app-palette",
title: "ACID ACCENT",
subtitle: "APP.04 // COLORWAY",
- icon: "brightness",
+ icon: "check",
description: "Primary phosphor luminescence wavelength (Acid Green).",
locked: false,
lockReason: "",
requires: ["app-motion"],
- pos: { x: 390, y: -100 },
+ pos: { x: 387, y: -130 },
edges: [],
controlType: "readonly",
value: function() { return 1; },
@@ -230,19 +228,19 @@ Item {
id: "desktop",
name: "DESKTOP",
subtitle: "WAYLAND SHELL // SURFACES",
- icon: "check",
+ icon: "power",
description: "Wayland compositor surfaces, Command Deck application launcher, and side hubs.",
nodes: [
{
id: "dt-compositor",
title: "WAYLAND CORE",
subtitle: "DT.01 // SERVER",
- icon: "check",
+ icon: "wifi",
description: "Native Wayland compositor session (Hyprland / Niri protocols).",
locked: false,
lockReason: "",
requires: [],
- pos: { x: 70, y: 0 },
+ pos: { x: 59, y: -28 },
edges: ["dt-command-deck", "dt-system-rail"],
controlType: "readonly",
value: function() { return 1; },
@@ -253,12 +251,12 @@ Item {
id: "dt-command-deck",
title: "COMMAND DECK",
subtitle: "DT.02 // LAUNCHER",
- icon: "check",
+ icon: "wifi",
description: "Fuzzy search launcher and system action execution console.",
locked: false,
lockReason: "",
requires: ["dt-compositor"],
- pos: { x: 230, y: -100 },
+ pos: { x: 238, y: -105 },
edges: ["dt-notifications"],
controlType: "toggle",
value: function() { return Settings.featuresCommandDeck; },
@@ -273,12 +271,12 @@ Item {
id: "dt-system-rail",
title: "SYSTEM RAIL",
subtitle: "DT.03 // SIDE HUB",
- icon: "check",
+ icon: "battery",
description: "Collapsible side panel with hardware controls and audio routing.",
locked: false,
lockReason: "",
requires: ["dt-compositor"],
- pos: { x: 230, y: 100 },
+ pos: { x: 248, y: 106 },
edges: [],
controlType: "toggle",
value: function() { return Settings.featuresSystemRail; },
@@ -293,12 +291,12 @@ Item {
id: "dt-notifications",
title: "ALERT TOASTS",
subtitle: "DT.04 // NOTIFIER",
- icon: "check",
+ icon: "warning",
description: "Tactical notification alert toasts and desktop signal daemon.",
locked: false,
lockReason: "",
requires: ["dt-command-deck"],
- pos: { x: 390, y: -100 },
+ pos: { x: 370, y: -93 },
edges: [],
controlType: "toggle",
value: function() { return Settings.featuresNotifications; },
@@ -319,19 +317,19 @@ Item {
id: "network",
name: "NETWORK",
subtitle: "INTERFACES // PACKET FLOW",
- icon: "wifi",
+ icon: "warning",
description: "NetworkManager client daemon, packet flow throughput, and subnet node tracer.",
nodes: [
{
id: "net-core",
title: "LINK ADAPTER",
subtitle: "NET.01 // INTERFACE",
- icon: "wifi",
+ icon: "microphone",
description: "Primary network interface connection and transport controller.",
locked: false,
lockReason: "",
requires: [],
- pos: { x: 70, y: 0 },
+ pos: { x: 75, y: -14 },
edges: ["net-flow", "net-tracer"],
controlType: "readonly",
value: function() { return NetworkService.isConnected ? 1 : 0; },
@@ -342,12 +340,12 @@ Item {
id: "net-flow",
title: "FLOW MATRIX",
subtitle: "NET.02 // BANDWIDTH",
- icon: "wifi",
+ icon: "check",
description: "Real-time bandwidth throughput matrix in ambient status bar.",
locked: false,
lockReason: "",
requires: ["net-core"],
- pos: { x: 230, y: -100 },
+ pos: { x: 224, y: -131 },
edges: ["net-sniffer"],
controlType: "toggle",
value: function() { return Settings.widgetNetworkFlowVisible; },
@@ -367,7 +365,7 @@ Item {
locked: false,
lockReason: "",
requires: ["net-core"],
- pos: { x: 230, y: 100 },
+ pos: { x: 267, y: 81 },
edges: [],
controlType: "toggle",
value: function() { return Settings.widgetNetworkTracerVisible; },
@@ -382,12 +380,12 @@ Item {
id: "net-sniffer",
title: "PACKET SNIFFER",
subtitle: "NET.04 // SURVEILLANCE",
- icon: "wifi",
+ icon: "microphone",
description: "Deep packet inspection and socket surveillance matrix.",
locked: true,
lockReason: "Requires raw CAP_NET_RAW / promiscuous socket privileges.",
requires: ["net-flow"],
- pos: { x: 390, y: -100 },
+ pos: { x: 418, y: -109 },
edges: [],
controlType: "readonly",
value: function() { return 0; },
@@ -404,19 +402,19 @@ Item {
id: "audio",
name: "AUDIO",
subtitle: "PIPEWIRE // SPECTRUM",
- icon: "volume",
+ icon: "warning",
description: "PipeWire sound server output sinks, microphone input gate, and FFT spectrum.",
nodes: [
{
id: "audio-master",
title: "OUTPUT SINK",
subtitle: "AUD.01 // PIPEWIRE",
- icon: "volume",
+ icon: "microphone",
description: "PipeWire master output stream sink: " + (AudioService.sinkName || "Default"),
locked: false,
lockReason: "",
requires: [],
- pos: { x: 70, y: 0 },
+ pos: { x: 50, y: 19 },
edges: ["audio-mute", "audio-spectrum"],
controlType: "slider",
minVal: 0,
@@ -437,12 +435,12 @@ Item {
id: "audio-mute",
title: "MUTE TOGGLE",
subtitle: "AUD.02 // ATTENUATION",
- icon: "volume",
+ icon: "power",
description: "Master output hardware gate toggle.",
locked: false,
lockReason: "",
requires: ["audio-master"],
- pos: { x: 230, y: -100 },
+ pos: { x: 238, y: -106 },
edges: ["audio-dsp"],
controlType: "toggle",
value: function() { return !AudioService.muted; },
@@ -456,12 +454,12 @@ Item {
id: "audio-spectrum",
title: "SPECTRUM SURVEILLANCE",
subtitle: "AUD.03 // FFT BARS",
- icon: "volume",
+ icon: "warning",
description: "Real-time FFT audio surveillance frequency visualization widget.",
locked: false,
lockReason: "",
requires: ["audio-master"],
- pos: { x: 230, y: 100 },
+ pos: { x: 261, y: 88 },
edges: [],
controlType: "toggle",
value: function() { return Settings.widgetAudioSurveillanceVisible; },
@@ -481,7 +479,7 @@ Item {
locked: true,
lockReason: "DSP kernel pipeline locked by audio server driver.",
requires: ["audio-mute"],
- pos: { x: 390, y: -100 },
+ pos: { x: 391, y: -133 },
edges: [],
controlType: "readonly",
value: function() { return 0; },
@@ -498,19 +496,19 @@ Item {
id: "input",
name: "INPUT",
subtitle: "LIBINPUT // HID DEVICES",
- icon: "gear",
+ icon: "check",
description: "Pointer acceleration, keyboard repeat rate, and multi-finger gestures.",
nodes: [
{
id: "in-engine",
title: "INPUT ENGINE",
subtitle: "INP.01 // LIBINPUT",
- icon: "gear",
+ icon: "lock",
description: "Kernel evdev abstraction and libinput driver layer.",
locked: false,
lockReason: "",
requires: [],
- pos: { x: 70, y: 0 },
+ pos: { x: 59, y: -36 },
edges: ["in-pointer", "in-repeat"],
controlType: "readonly",
value: function() { return 1; },
@@ -521,12 +519,12 @@ Item {
id: "in-pointer",
title: "POINTER SENSE",
subtitle: "INP.02 // CURSOR",
- icon: "gear",
+ icon: "wifi",
description: "Pointer acceleration profile and sensitivity curves.",
locked: false,
lockReason: "",
requires: ["in-engine"],
- pos: { x: 230, y: -100 },
+ pos: { x: 230, y: -89 },
edges: ["in-gestures"],
controlType: "action",
actionLabel: "CYCLE PROFILE",
@@ -540,12 +538,12 @@ Item {
id: "in-repeat",
title: "KEY REPEAT",
subtitle: "INP.03 // TYPEMATIC",
- icon: "gear",
+ icon: "brightness",
description: "Keyboard repeat delay and typematic strike frequency.",
locked: false,
lockReason: "",
requires: ["in-engine"],
- pos: { x: 230, y: 100 },
+ pos: { x: 224, y: 68 },
edges: [],
controlType: "readonly",
value: function() { return 1; },
@@ -556,12 +554,12 @@ Item {
id: "in-gestures",
title: "TOUCH GESTURES",
subtitle: "INP.04 // PRECISION",
- icon: "gear",
+ icon: "power",
description: "Multi-touch workspace navigation and boundary swipe recognition.",
locked: true,
lockReason: "No supported precision touchpad hardware detected.",
requires: ["in-pointer"],
- pos: { x: 390, y: -100 },
+ pos: { x: 377, y: -68 },
edges: [],
controlType: "readonly",
value: function() { return 0; },
@@ -578,19 +576,19 @@ Item {
id: "power",
name: "POWER",
subtitle: "UPOWER // GOVERNOR",
- icon: "power",
+ icon: "gear",
description: "Energy governor, battery charge conservation, and display DPMS timeouts.",
nodes: [
{
id: "pwr-governor",
title: "ENERGY DAEMON",
subtitle: "PWR.01 // UPOWER",
- icon: "power",
+ icon: "lock",
description: "UPower daemon connection and system power supply status.",
locked: false,
lockReason: "",
requires: [],
- pos: { x: 70, y: 0 },
+ pos: { x: 70, y: -13 },
edges: ["pwr-supply", "pwr-sleep"],
controlType: "readonly",
value: function() { return PowerService.available ? 1 : 0; },
@@ -601,12 +599,12 @@ Item {
id: "pwr-supply",
title: "BATTERY CAPACITY",
subtitle: "PWR.02 // CHARGE",
- icon: "power",
+ icon: "brightness",
description: "Lithium-ion energy storage cell state and charge level.",
locked: false,
lockReason: "",
requires: ["pwr-governor"],
- pos: { x: 230, y: -100 },
+ pos: { x: 253, y: -90 },
edges: ["pwr-threshold"],
controlType: "readonly",
value: function() { return PowerService.isBatteryPresent ? PowerService.percentage : 100; },
@@ -617,12 +615,12 @@ Item {
id: "pwr-sleep",
title: "IDLE BLANKING",
subtitle: "PWR.03 // DPMS",
- icon: "power",
+ icon: "warning",
description: "Display DPMS timeout and screen power conservation.",
locked: false,
lockReason: "",
requires: ["pwr-governor"],
- pos: { x: 230, y: 100 },
+ pos: { x: 248, y: 78 },
edges: [],
controlType: "readonly",
value: function() { return 1; },
@@ -633,12 +631,12 @@ Item {
id: "pwr-threshold",
title: "CHARGE THRESHOLD",
subtitle: "PWR.04 // CONSERVATION",
- icon: "power",
+ icon: "gear",
description: "Firmware-level 80% battery longevity threshold limit.",
locked: true,
lockReason: "Requires ACPI battery charge threshold driver support.",
requires: ["pwr-supply"],
- pos: { x: 390, y: -100 },
+ pos: { x: 383, y: -123 },
edges: [],
controlType: "readonly",
value: function() { return 0; },
@@ -655,19 +653,19 @@ Item {
id: "security",
name: "SECURITY",
subtitle: "PAM AUTH // PRIVACY",
- icon: "lock",
+ icon: "battery",
description: "Session lock supervisor, notification privacy filters, and encrypted vault.",
nodes: [
{
id: "sec-subsystem",
title: "SUPERVISOR",
subtitle: "SEC.01 // PAM",
- icon: "lock",
+ icon: "battery",
description: "ctOS session security supervisor and authentication guard.",
locked: false,
lockReason: "",
requires: [],
- pos: { x: 70, y: 0 },
+ pos: { x: 61, y: 31 },
edges: ["sec-lock", "sec-privacy"],
controlType: "readonly",
value: function() { return 1; },
@@ -678,12 +676,12 @@ Item {
id: "sec-lock",
title: "SESSION LOCK",
subtitle: "SEC.02 // DISPLAY",
- icon: "lock",
+ icon: "power",
description: "Immediately engage loginctl lock screen overlay.",
locked: false,
lockReason: "",
requires: ["sec-subsystem"],
- pos: { x: 230, y: -100 },
+ pos: { x: 258, y: -107 },
edges: ["sec-vault"],
controlType: "action",
actionLabel: "LOCK SESSION",
@@ -698,12 +696,12 @@ Item {
id: "sec-privacy",
title: "ALERT PRIVACY",
subtitle: "SEC.03 // COOLDOWN",
- icon: "lock",
+ icon: "battery",
description: "Enforce anti-spam cooldown intervals on notification popups.",
locked: false,
lockReason: "",
requires: ["sec-subsystem"],
- pos: { x: 230, y: 100 },
+ pos: { x: 264, y: 114 },
edges: [],
controlType: "toggle",
value: function() { return Settings.notificationCooldownSeconds > 0; },
@@ -718,12 +716,91 @@ Item {
id: "sec-vault",
title: "ENCRYPTED VAULT",
subtitle: "SEC.04 // LUKS-HSM",
- icon: "lock",
+ icon: "gear",
description: "Hardware security module and encrypted credentials vault.",
locked: true,
lockReason: "Requires LUKS hardware token or biometric key-ring authorization.",
requires: ["sec-lock"],
- pos: { x: 390, y: -100 },
+ pos: { x: 424, y: -89 },
+ edges: [],
+ controlType: "readonly",
+ value: function() { return 0; },
+ valueText: function() { return "RESTRICTED"; },
+ execute: function() {}
+ }
+ ]
+
+ },
+ // =====================================================================
+ // 8. DEV
+ // =====================================================================
+ {
+ id: "dev",
+ name: "DEV",
+ subtitle: "DEVELOPMENT // DEBUG",
+ icon: "gear",
+ description: "Developer tools and debug overlays.",
+ nodes: [
+ {
+ id: "dev-debug",
+ title: "DEBUG OVERLAY",
+ subtitle: "DEV.01 // HUD",
+ icon: "check",
+ description: "Toggle debug overlay.",
+ locked: false,
+ lockReason: "",
+ requires: [],
+ pos: { x: 81, y: -2 },
+ edges: ["dev-log", "dev-trace"],
+ controlType: "toggle",
+ value: function() { return 0; },
+ valueText: function() { return "DISABLED"; },
+ execute: function() {}
+ },
+ {
+ id: "dev-log",
+ title: "SYSTEM LOG",
+ subtitle: "DEV.02 // LOG",
+ icon: "lock",
+ description: "View system logs.",
+ locked: false,
+ lockReason: "",
+ requires: ["dev-debug"],
+ pos: { x: 187, y: -65 },
+ edges: ["dev-console"],
+ controlType: "action",
+ actionLabel: "OPEN",
+ value: function() { return 1; },
+ valueText: function() { return "LOG"; },
+ execute: function() {}
+ },
+ {
+ id: "dev-trace",
+ title: "PROFILER TRACE",
+ subtitle: "DEV.03 // TRACE",
+ icon: "wifi",
+ description: "Trace process execution.",
+ locked: false,
+ lockReason: "",
+ requires: ["dev-debug"],
+ pos: { x: 283, y: 101 },
+ edges: [],
+ controlType: "action",
+ actionLabel: "TRACE",
+ value: function() { return 1; },
+ valueText: function() { return "TRACE"; },
+ execute: function() {}
+ },
+ {
+ id: "dev-console",
+ title: "ROOT CONSOLE",
+ subtitle: "DEV.04 // ROOT",
+ icon: "volume",
+ description: "Root shell access.",
+ locked: true,
+ lockReason: "Requires sudo.",
+ requires: ["dev-log"],
+ pos: { x: 346, y: -76 },
edges: [],
controlType: "readonly",
value: function() { return 0; },
@@ -734,6 +811,9 @@ Item {
}
]
+
+ readonly property int categoryCount: categories ? categories.length : 0
+
function getCategory(index) {
if (index < 0 || index >= categories.length) return null;
return categories[index];
diff --git a/shell/desktop/surfaces/radial/SkillNode.qml b/shell/desktop/surfaces/radial/SkillNode.qml
index eda20312..3e9bd577 100644
--- a/shell/desktop/surfaces/radial/SkillNode.qml
+++ b/shell/desktop/surfaces/radial/SkillNode.qml
@@ -158,22 +158,5 @@ Item {
}
}
- // Interactive Hover and Click Handler
- MouseArea {
- id: nodeMouseArea
- anchors.fill: parent
- hoverEnabled: true
- cursorShape: Qt.PointingHandCursor
- onEntered: {
- root.isHovered = true;
- root.hovered();
- }
- onExited: {
- root.isHovered = false;
- }
- onClicked: {
- root.clicked();
- }
- }
}
diff --git a/shell/desktop/surfaces/radial/SkillTree.qml b/shell/desktop/surfaces/radial/SkillTree.qml
index a2e8ae14..62ce456a 100644
--- a/shell/desktop/surfaces/radial/SkillTree.qml
+++ b/shell/desktop/surfaces/radial/SkillTree.qml
@@ -64,66 +64,75 @@ Item {
}
Repeater {
- model: 8
+ model: root.model ? root.model.categoryCount : 0
delegate: Item {
id: previewSubtree
required property int index
readonly property int categoryIdx: previewSubtree.index
- readonly property real angleDeg: -90.0 + previewSubtree.categoryIdx * 45.0
+ readonly property real angleDeg: -90.0 + previewSubtree.categoryIdx * (360.0 / (root.model ? root.model.categoryCount : 8))
readonly property bool isCatFocused: root.focusedCategoryIndex === previewSubtree.categoryIdx
readonly property var cat: (root.model && root.model.categories[previewSubtree.categoryIdx]) ? root.model.categories[previewSubtree.categoryIdx] : null
// Subtree polar anchor points
- readonly property var r0Point: RadialGeometry.pointOnCircle(root.wheelCenterX, root.wheelCenterY, 175, previewSubtree.angleDeg)
- readonly property var r1Point: RadialGeometry.pointOnCircle(root.wheelCenterX, root.wheelCenterY, 215, previewSubtree.angleDeg)
- readonly property var c1Point: RadialGeometry.pointOnCircle(root.wheelCenterX, root.wheelCenterY, 260, previewSubtree.angleDeg - 8.0)
- readonly property var c2Point: RadialGeometry.pointOnCircle(root.wheelCenterX, root.wheelCenterY, 260, previewSubtree.angleDeg + 8.0)
- readonly property var c3Point: RadialGeometry.pointOnCircle(root.wheelCenterX, root.wheelCenterY, 305, previewSubtree.angleDeg - 12.0)
+ property var r0Point: ({x: 0, y: 0})
+ property var r1Point: ({x: 0, y: 0})
+ property var c1Point: ({x: 0, y: 0})
+ property var c2Point: ({x: 0, y: 0})
+ property var c3Point: ({x: 0, y: 0})
+
+ Component.onCompleted: {
+ var a = previewSubtree.angleDeg;
+ r0Point = RadialGeometry.pointOnCircle(0, 0, 206, a);
+ r1Point = RadialGeometry.pointOnCircle(0, 0, 246, a);
+ c1Point = RadialGeometry.pointOnCircle(0, 0, 291, a - 8.0);
+ c2Point = RadialGeometry.pointOnCircle(0, 0, 291, a + 8.0);
+ c3Point = RadialGeometry.pointOnCircle(0, 0, 336, a - 12.0);
+ }
// Trunk edge
SkillEdge {
- x1: previewSubtree.r0Point.x
- y1: previewSubtree.r0Point.y
- x2: previewSubtree.r1Point.x
- y2: previewSubtree.r1Point.y
+ x1: (root.wheelCenterX + previewSubtree.r0Point.x)
+ y1: (root.wheelCenterY + previewSubtree.r0Point.y)
+ x2: (root.wheelCenterX + previewSubtree.r1Point.x)
+ y2: (root.wheelCenterY + previewSubtree.r1Point.y)
isActive: previewSubtree.isCatFocused
isPreview: true
}
// Branch edge 1
SkillEdge {
- x1: previewSubtree.r1Point.x
- y1: previewSubtree.r1Point.y
- x2: previewSubtree.c1Point.x
- y2: previewSubtree.c1Point.y
+ x1: (root.wheelCenterX + previewSubtree.r1Point.x)
+ y1: (root.wheelCenterY + previewSubtree.r1Point.y)
+ x2: (root.wheelCenterX + previewSubtree.c1Point.x)
+ y2: (root.wheelCenterY + previewSubtree.c1Point.y)
isActive: previewSubtree.isCatFocused
isPreview: true
}
// Branch edge 2
SkillEdge {
- x1: previewSubtree.r1Point.x
- y1: previewSubtree.r1Point.y
- x2: previewSubtree.c2Point.x
- y2: previewSubtree.c2Point.y
+ x1: (root.wheelCenterX + previewSubtree.r1Point.x)
+ y1: (root.wheelCenterY + previewSubtree.r1Point.y)
+ x2: (root.wheelCenterX + previewSubtree.c2Point.x)
+ y2: (root.wheelCenterY + previewSubtree.c2Point.y)
isActive: previewSubtree.isCatFocused
isPreview: true
}
// Branch edge 3 (Leaf extension)
SkillEdge {
- x1: previewSubtree.c1Point.x
- y1: previewSubtree.c1Point.y
- x2: previewSubtree.c3Point.x
- y2: previewSubtree.c3Point.y
+ x1: (root.wheelCenterX + previewSubtree.c1Point.x)
+ y1: (root.wheelCenterY + previewSubtree.c1Point.y)
+ x2: (root.wheelCenterX + previewSubtree.c3Point.x)
+ y2: (root.wheelCenterY + previewSubtree.c3Point.y)
isActive: previewSubtree.isCatFocused
isPreview: true
}
// Root Preview Node
SkillNode {
- x: previewSubtree.r1Point.x - width / 2
- y: previewSubtree.r1Point.y - height / 2
+ x: (root.wheelCenterX + previewSubtree.r1Point.x) - width / 2
+ y: (root.wheelCenterY + previewSubtree.r1Point.y) - height / 2
isPreview: true
isSelected: previewSubtree.isCatFocused
iconName: previewSubtree.cat ? previewSubtree.cat.icon : "gear"
@@ -131,8 +140,8 @@ Item {
// Child Preview Node 1
SkillNode {
- x: previewSubtree.c1Point.x - width / 2
- y: previewSubtree.c1Point.y - height / 2
+ x: (root.wheelCenterX + previewSubtree.c1Point.x) - width / 2
+ y: (root.wheelCenterY + previewSubtree.c1Point.y) - height / 2
isPreview: true
isSelected: previewSubtree.isCatFocused
iconName: (previewSubtree.cat && previewSubtree.cat.nodes && previewSubtree.cat.nodes[1]) ? previewSubtree.cat.nodes[1].icon : (previewSubtree.cat ? previewSubtree.cat.icon : "gear")
@@ -140,8 +149,8 @@ Item {
// Child Preview Node 2
SkillNode {
- x: previewSubtree.c2Point.x - width / 2
- y: previewSubtree.c2Point.y - height / 2
+ x: (root.wheelCenterX + previewSubtree.c2Point.x) - width / 2
+ y: (root.wheelCenterY + previewSubtree.c2Point.y) - height / 2
isPreview: true
isSelected: previewSubtree.isCatFocused
iconName: (previewSubtree.cat && previewSubtree.cat.nodes && previewSubtree.cat.nodes[2]) ? previewSubtree.cat.nodes[2].icon : (previewSubtree.cat ? previewSubtree.cat.icon : "gear")
@@ -149,8 +158,8 @@ Item {
// Child Preview Node 3
SkillNode {
- x: previewSubtree.c3Point.x - width / 2
- y: previewSubtree.c3Point.y - height / 2
+ x: (root.wheelCenterX + previewSubtree.c3Point.x) - width / 2
+ y: (root.wheelCenterY + previewSubtree.c3Point.y) - height / 2
isPreview: true
isSelected: previewSubtree.isCatFocused
iconName: (previewSubtree.cat && previewSubtree.cat.nodes && previewSubtree.cat.nodes[3]) ? previewSubtree.cat.nodes[3].icon : (previewSubtree.cat ? previewSubtree.cat.icon : "gear")
From e90924eddc81a8bb3eac63951237c7ed81ebf820 Mon Sep 17 00:00:00 2001
From: reze-dev <25588579+reze-dev@users.noreply.github.com>
Date: Sat, 26 Sep 2026 17:51:59 +0530
Subject: [PATCH 13/48] =?UTF-8?q?feat(radial):=20complete=20UI=20overhaul?=
=?UTF-8?q?=20=E2=80=94=20Material=20Icons,=20transitions,=20edges,=20navi?=
=?UTF-8?q?gation?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
Milestone 1 (R3): Replace all 21 system SVGs with Google Material Symbols
Outlined (weight 300, optical size 24). Add 7 new icons (cpu, keyboard,
memory, mouse, palette, shield, terminal). Fix CtosIcon.qml colorization
so icons correctly tint to dark/acid green.
Milestone 2 (R1): Fix choreographed expand/collapse transitions with
shortest-path rotation, smooth left-edge translation, and outward branch
deployment under 700ms. Remove readonly property var anti-pattern.
Milestone 3 (R2): Fix edge alignment — connecting lines now lock onto
node centers in preview mode, expanded mode, and during transitions.
Milestone 4 (R4): Fix spatial navigation — distance-weighted arrow key
traversal, jitter-free hover snapping, scroll wheel node traversal,
and smooth ContextPanel content transitions.
Audit: 81/81 pass, qmllint clean on all files.
---
.../desktop/assets/icons/battery-charging.svg | 8 +-
shell/desktop/assets/icons/battery-low.svg | 8 +-
shell/desktop/assets/icons/battery.svg | 7 +-
.../desktop/assets/icons/bluetooth-slash.svg | 7 +-
shell/desktop/assets/icons/bluetooth.svg | 6 +-
shell/desktop/assets/icons/brightness.svg | 7 +-
shell/desktop/assets/icons/check.svg | 6 +-
shell/desktop/assets/icons/close.svg | 6 +-
shell/desktop/assets/icons/cpu.svg | 5 +
shell/desktop/assets/icons/gear.svg | 8 +-
shell/desktop/assets/icons/keyboard.svg | 5 +
shell/desktop/assets/icons/lock.svg | 9 +-
shell/desktop/assets/icons/logout.svg | 8 +-
shell/desktop/assets/icons/memory.svg | 5 +
.../desktop/assets/icons/microphone-slash.svg | 11 +-
shell/desktop/assets/icons/microphone.svg | 9 +-
shell/desktop/assets/icons/mouse.svg | 5 +
shell/desktop/assets/icons/palette.svg | 5 +
shell/desktop/assets/icons/power.svg | 7 +-
shell/desktop/assets/icons/reboot.svg | 9 +-
shell/desktop/assets/icons/shield.svg | 5 +
shell/desktop/assets/icons/terminal.svg | 5 +
shell/desktop/assets/icons/volume-mute.svg | 8 +-
shell/desktop/assets/icons/volume-slash.svg | 8 +-
shell/desktop/assets/icons/volume.svg | 8 +-
shell/desktop/assets/icons/warning.svg | 8 +-
shell/desktop/assets/icons/wifi-slash.svg | 12 +-
shell/desktop/assets/icons/wifi.svg | 9 +-
.../desktop/surfaces/components/CtosIcon.qml | 35 ++---
.../surfaces/radial/CircularSettingsMenu.qml | 39 +++--
.../desktop/surfaces/radial/ContextPanel.qml | 21 +++
.../surfaces/radial/NavigationController.qml | 4 +-
.../desktop/surfaces/radial/RadialGeometry.js | 43 +++++-
.../desktop/surfaces/radial/RadialSegment.qml | 16 +--
.../surfaces/radial/RadialSettings.qml | 87 +++++++++++-
shell/desktop/surfaces/radial/SkillNode.qml | 23 ++-
shell/desktop/surfaces/radial/SkillTree.qml | 133 ++++++++++++------
37 files changed, 419 insertions(+), 186 deletions(-)
create mode 100644 shell/desktop/assets/icons/cpu.svg
create mode 100644 shell/desktop/assets/icons/keyboard.svg
create mode 100644 shell/desktop/assets/icons/memory.svg
create mode 100644 shell/desktop/assets/icons/mouse.svg
create mode 100644 shell/desktop/assets/icons/palette.svg
create mode 100644 shell/desktop/assets/icons/shield.svg
create mode 100644 shell/desktop/assets/icons/terminal.svg
diff --git a/shell/desktop/assets/icons/battery-charging.svg b/shell/desktop/assets/icons/battery-charging.svg
index 6c8b5a4e..c32c4761 100644
--- a/shell/desktop/assets/icons/battery-charging.svg
+++ b/shell/desktop/assets/icons/battery-charging.svg
@@ -1,5 +1,5 @@
-
\ No newline at end of file
diff --git a/shell/desktop/assets/icons/volume-slash.svg b/shell/desktop/assets/icons/volume-slash.svg
index c626441d..1dd25e69 100644
--- a/shell/desktop/assets/icons/volume-slash.svg
+++ b/shell/desktop/assets/icons/volume-slash.svg
@@ -1,5 +1,5 @@
-
+
-
+
\ No newline at end of file
diff --git a/shell/desktop/assets/icons/volume.svg b/shell/desktop/assets/icons/volume.svg
index 496ebc11..1bbb12b8 100644
--- a/shell/desktop/assets/icons/volume.svg
+++ b/shell/desktop/assets/icons/volume.svg
@@ -1,5 +1,5 @@
-
+
-
+
\ No newline at end of file
diff --git a/shell/desktop/assets/icons/vpn-key.svg b/shell/desktop/assets/icons/vpn-key.svg
index 52d8df61..c315817f 100644
--- a/shell/desktop/assets/icons/vpn-key.svg
+++ b/shell/desktop/assets/icons/vpn-key.svg
@@ -1 +1 @@
-
\ No newline at end of file
+
\ No newline at end of file
diff --git a/shell/desktop/assets/icons/warning.svg b/shell/desktop/assets/icons/warning.svg
index 9b21ce12..82c4ed3b 100644
--- a/shell/desktop/assets/icons/warning.svg
+++ b/shell/desktop/assets/icons/warning.svg
@@ -1,5 +1,5 @@
-
+
-
+
\ No newline at end of file
diff --git a/shell/desktop/assets/icons/wifi-slash.svg b/shell/desktop/assets/icons/wifi-slash.svg
index db5cdf8f..733d6ca7 100644
--- a/shell/desktop/assets/icons/wifi-slash.svg
+++ b/shell/desktop/assets/icons/wifi-slash.svg
@@ -1,5 +1,5 @@
-
+
-
+
\ No newline at end of file
diff --git a/shell/desktop/assets/icons/wifi.svg b/shell/desktop/assets/icons/wifi.svg
index a3147aab..0fc6ba6d 100644
--- a/shell/desktop/assets/icons/wifi.svg
+++ b/shell/desktop/assets/icons/wifi.svg
@@ -1,5 +1,5 @@
-
+
-
+
\ No newline at end of file
From 976ec34585e7f07f5cca9430bba1cef48d7a41cf Mon Sep 17 00:00:00 2001
From: reze-dev <25588579+reze-dev@users.noreply.github.com>
Date: Mon, 28 Sep 2026 11:49:37 +0530
Subject: [PATCH 41/48] feat(ui): dynamic island morphs into tabbed event
logger
---
shell/desktop/surfaces/AmbientBar.qml | 71 +++++++++-
shell/desktop/surfaces/EventLog.qml | 126 ++++++++++++++++--
.../surfaces/components/DynamicIsland.qml | 36 ++++-
shell/shell.qml | 12 +-
tests/e2e/tier1_features/test_t8_event_log.sh | 8 +-
.../tier1_features/test_t8_shell_wiring.sh | 10 +-
6 files changed, 222 insertions(+), 41 deletions(-)
diff --git a/shell/desktop/surfaces/AmbientBar.qml b/shell/desktop/surfaces/AmbientBar.qml
index c8e906c2..276cc4a6 100644
--- a/shell/desktop/surfaces/AmbientBar.qml
+++ b/shell/desktop/surfaces/AmbientBar.qml
@@ -1,6 +1,7 @@
import QtQuick
import QtQuick.Layouts
import Quickshell
+import Quickshell.Wayland
import "../core"
import "../services"
import "./components"
@@ -13,8 +14,65 @@ PanelWindow {
signal toggleNetwork
color: "transparent"
- focusable: true
- implicitHeight: Theme.barHeight
+
+ property bool toggleMask: false
+
+ Component {
+ id: maskComponent
+ Region {
+ Region {
+ x: leftSections.x
+ y: leftSections.y
+ width: leftSections.width
+ height: leftSections.height
+ }
+ Region {
+ x: centerSection.x
+ y: centerSection.y
+ width: centerSection.isEventLogOpen ? 360 : centerSection.width
+ height: centerSection.isEventLogOpen ? 500 : centerSection.height
+ }
+ Region {
+ x: rightSections.x
+ y: rightSections.y
+ width: rightSections.width
+ height: rightSections.height
+ }
+ }
+ }
+
+ property var maskA: maskComponent.createObject(root)
+ property var maskB: maskComponent.createObject(root)
+
+ mask: toggleMask ? maskA : maskB
+
+ function flushWaylandMask() {
+ toggleMask = !toggleMask;
+ }
+
+ Connections {
+ target: leftSections
+ function onWidthChanged() { root.flushWaylandMask(); }
+ function onXChanged() { root.flushWaylandMask(); }
+ }
+ Connections {
+ target: centerSection
+ function onWidthChanged() { root.flushWaylandMask(); }
+ function onHeightChanged() { root.flushWaylandMask(); }
+ function onXChanged() { root.flushWaylandMask(); }
+ function onIsEventLogOpenChanged() { root.flushWaylandMask(); }
+ }
+ Connections {
+ target: rightSections
+ function onWidthChanged() { root.flushWaylandMask(); }
+ function onXChanged() { root.flushWaylandMask(); }
+ }
+ WlrLayershell.namespace: "ctos-bar"
+ WlrLayershell.keyboardFocus: centerSection.isEventLogOpen ? WlrKeyboardFocus.OnDemand : WlrKeyboardFocus.None
+ implicitHeight: (centerSection.isEventLogOpen || centerSection.height > Theme.barHeight) ? 506 : Theme.barHeight
+
+ exclusionMode: ExclusionMode.Normal
+ exclusiveZone: Theme.barHeight
anchors {
left: true
@@ -35,7 +93,8 @@ PanelWindow {
anchors.left: parent.left
anchors.leftMargin: Theme.barPaddingHorizontal
- anchors.verticalCenter: parent.verticalCenter
+ anchors.top: parent.top
+ anchors.topMargin: (Theme.barHeight - height) / 2
spacing: Theme.spacingMedium
// Section 1: Blume Logo
@@ -146,7 +205,8 @@ PanelWindow {
id: centerSection
anchors.horizontalCenter: parent.horizontalCenter
- anchors.verticalCenter: parent.verticalCenter
+ anchors.top: parent.top
+ anchors.topMargin: (Theme.barHeight - (Theme.barHeight - 6)) / 2
}
// =========================================================================
@@ -158,7 +218,8 @@ PanelWindow {
anchors.right: parent.right
anchors.rightMargin: Theme.barPaddingHorizontal
- anchors.verticalCenter: parent.verticalCenter
+ anchors.top: parent.top
+ anchors.topMargin: (Theme.barHeight - height) / 2
spacing: Theme.spacingMedium
// Section 4: Network
diff --git a/shell/desktop/surfaces/EventLog.qml b/shell/desktop/surfaces/EventLog.qml
index 1eba6f0a..141ecee3 100644
--- a/shell/desktop/surfaces/EventLog.qml
+++ b/shell/desktop/surfaces/EventLog.qml
@@ -13,10 +13,12 @@ FocusScope {
implicitWidth: 360
width: 360
- anchors.bottom: parent ? parent.bottom : undefined
- anchors.top: parent ? parent.top : undefined
+ implicitHeight: 500
+ height: 500
focus: true
+ property int currentTab: 0
+
// Keyboard navigation focus & Tiered Escape trapping
Keys.onEscapePressed: function (event) {
OverlayController.close();
@@ -42,14 +44,7 @@ FocusScope {
}
}
- // Background container
- Rectangle {
- id: panelBackground
- anchors.fill: parent
- color: Theme.gray900
- border.color: Theme.borderMuted
- border.width: Theme.borderWidth
- }
+ // Background container handled by Dynamic Island
// Corner Brackets decoration (cyber aesthetic)
CornerBrackets {
@@ -158,11 +153,81 @@ FocusScope {
}
}
- // Header Divider
- Rectangle {
+ // Header Divider & Tabs
+ ColumnLayout {
Layout.fillWidth: true
- Layout.preferredHeight: Theme.borderWidth
- color: Theme.borderMuted
+ spacing: 0
+
+ RowLayout {
+ Layout.fillWidth: true
+ spacing: Theme.spacingMedium
+
+ Rectangle {
+ id: tab0
+ Layout.preferredHeight: 24
+ Layout.preferredWidth: tab0Text.implicitWidth + Theme.paddingMedium * 2
+ color: root.currentTab === 0 ? Theme.surfaceSelected : (tab0MouseArea.containsMouse ? Theme.surfaceHover : "transparent")
+ border.color: root.currentTab === 0 ? Theme.accent : Theme.borderMuted
+ border.width: Theme.borderWidth
+ radius: Theme.radiusSmall
+
+ Text {
+ id: tab0Text
+ anchors.centerIn: parent
+ color: root.currentTab === 0 ? Theme.accent : Theme.textSecondary
+ font.family: Theme.fontFamilyMonospace
+ font.pixelSize: Theme.fontSizeCaption
+ font.weight: Theme.fontWeightBold
+ text: "NOTIFICATIONS"
+ }
+
+ MouseArea {
+ id: tab0MouseArea
+ anchors.fill: parent
+ hoverEnabled: true
+ cursorShape: Qt.PointingHandCursor
+ onClicked: root.currentTab = 0
+ }
+ }
+
+ Rectangle {
+ id: tab1
+ Layout.preferredHeight: 24
+ Layout.preferredWidth: tab1Text.implicitWidth + Theme.paddingMedium * 2
+ color: root.currentTab === 1 ? Theme.surfaceSelected : (tab1MouseArea.containsMouse ? Theme.surfaceHover : "transparent")
+ border.color: root.currentTab === 1 ? Theme.accent : Theme.borderMuted
+ border.width: Theme.borderWidth
+ radius: Theme.radiusSmall
+
+ Text {
+ id: tab1Text
+ anchors.centerIn: parent
+ color: root.currentTab === 1 ? Theme.accent : Theme.textSecondary
+ font.family: Theme.fontFamilyMonospace
+ font.pixelSize: Theme.fontSizeCaption
+ font.weight: Theme.fontWeightBold
+ text: "SYSTEM LOGS"
+ }
+
+ MouseArea {
+ id: tab1MouseArea
+ anchors.fill: parent
+ hoverEnabled: true
+ cursorShape: Qt.PointingHandCursor
+ onClicked: root.currentTab = 1
+ }
+ }
+ }
+
+ Item {
+ Layout.preferredHeight: Theme.spacingMedium
+ }
+
+ Rectangle {
+ Layout.fillWidth: true
+ Layout.preferredHeight: Theme.borderWidth
+ color: Theme.borderMuted
+ }
}
// Body Content Area (ListView or Empty State)
@@ -170,6 +235,7 @@ FocusScope {
id: contentContainer
Layout.fillWidth: true
Layout.fillHeight: true
+ visible: root.currentTab === 0
// Notification History ListView
ListView {
@@ -372,17 +438,49 @@ FocusScope {
}
}
+ // System Logs Content Area
+ Item {
+ id: systemLogsContainer
+ Layout.fillWidth: true
+ Layout.fillHeight: true
+ visible: root.currentTab === 1
+
+ ColumnLayout {
+ anchors.centerIn: parent
+ spacing: Theme.spacingMedium
+
+ Rectangle {
+ Layout.alignment: Qt.AlignHCenter
+ Layout.preferredHeight: 8
+ Layout.preferredWidth: 8
+ color: Theme.textMuted
+ radius: 4
+ }
+
+ Text {
+ Layout.alignment: Qt.AlignHCenter
+ color: Theme.textMuted
+ font.family: Theme.fontFamilyMonospace
+ font.pixelSize: Theme.fontSizeSmall
+ font.weight: Theme.fontWeightMedium
+ text: "SYSTEM LOGS // UNAVAILABLE"
+ }
+ }
+ }
+
// Footer Divider
Rectangle {
Layout.fillWidth: true
Layout.preferredHeight: Theme.borderWidth
color: Theme.borderMuted
+ visible: root.currentTab === 0
}
// Footer Action Row
RowLayout {
Layout.fillWidth: true
spacing: Theme.spacingMedium
+ visible: root.currentTab === 0
Text {
Layout.fillWidth: true
diff --git a/shell/desktop/surfaces/components/DynamicIsland.qml b/shell/desktop/surfaces/components/DynamicIsland.qml
index c6f647ab..850d2e4e 100644
--- a/shell/desktop/surfaces/components/DynamicIsland.qml
+++ b/shell/desktop/surfaces/components/DynamicIsland.qml
@@ -20,10 +20,11 @@ Rectangle {
property string latestAppName: ""
property string latestSummary: ""
property int latestUrgency: 1
+ readonly property bool isEventLogOpen: OverlayController.activeSurface === OverlayController.Surface.EventLog
- width: isExpanded ? expandedWidth : compactWidth
+ width: isEventLogOpen ? 360 : (isExpanded ? expandedWidth : compactWidth)
implicitWidth: width
- height: Theme.barHeight - 6
+ height: isEventLogOpen ? 500 : (Theme.barHeight - 6)
implicitHeight: height
color: Theme.background
@@ -43,6 +44,13 @@ Rectangle {
}
}
+ Behavior on height {
+ NumberAnimation {
+ duration: Settings.reducedMotion ? 0 : Theme.durationSlow
+ easing.type: Easing.InOutQuad
+ }
+ }
+
// Auto-collapse after 4 seconds
Timer {
id: collapseTimer
@@ -98,7 +106,7 @@ Rectangle {
RowLayout {
id: compactContent
anchors.centerIn: parent
- opacity: root.isExpanded ? 0.0 : 1.0
+ opacity: (root.isExpanded || root.isEventLogOpen) ? 0.0 : 1.0
visible: opacity > 0.0
spacing: Theme.spacingSmall
@@ -171,7 +179,7 @@ Rectangle {
anchors.fill: parent
anchors.leftMargin: Theme.paddingLarge
anchors.rightMargin: Theme.paddingLarge
- opacity: root.isExpanded ? 1.0 : 0.0
+ opacity: (root.isExpanded && !root.isEventLogOpen) ? 1.0 : 0.0
visible: opacity > 0.0
spacing: Theme.spacingSmall
@@ -232,6 +240,7 @@ Rectangle {
cursorShape: Qt.PointingHandCursor
hoverEnabled: true
acceptedButtons: Qt.LeftButton | Qt.RightButton
+ enabled: !root.isEventLogOpen // Disable click to toggle when event log is fully open, as it handles its own input.
onClicked: (mouse) => {
if (mouse.button === Qt.RightButton) {
@@ -247,4 +256,23 @@ Rectangle {
}
}
}
+
+ // =========================================================================
+ // Event Log Container
+ // =========================================================================
+
+ Loader {
+ id: eventLogEmbeddedLoader
+ anchors.fill: parent
+ active: root.isEventLogOpen || opacity > 0
+ opacity: root.isEventLogOpen ? 1.0 : 0.0
+ visible: opacity > 0.0
+ source: "../EventLog.qml"
+
+ Behavior on opacity {
+ NumberAnimation {
+ duration: Theme.durationFast
+ }
+ }
+ }
}
diff --git a/shell/shell.qml b/shell/shell.qml
index 762fca57..e7ade314 100644
--- a/shell/shell.qml
+++ b/shell/shell.qml
@@ -431,6 +431,8 @@ Scope {
}
}
+
+
Variants {
id: barVariants
@@ -615,16 +617,8 @@ Scope {
source: "desktop/surfaces/SystemRail.qml"
}
- Loader {
- id: eventLogLoader
+ // EventLog moved to DynamicIsland
- anchors.bottom: parent.bottom
- anchors.right: parent.right
- anchors.top: parent.top
- asynchronous: false
- source: "desktop/surfaces/EventLog.qml"
- visible: OverlayController.activeSurface === OverlayController.Surface.EventLog
- }
Loader {
id: radialSettingsLoader
diff --git a/tests/e2e/tier1_features/test_t8_event_log.sh b/tests/e2e/tier1_features/test_t8_event_log.sh
index 8e69d1d7..fe469ec6 100644
--- a/tests/e2e/tier1_features/test_t8_event_log.sh
+++ b/tests/e2e/tier1_features/test_t8_event_log.sh
@@ -26,10 +26,10 @@ assert_grep "implicitWidth:\s*360" "${EVENT_LOG_QML}" "implicitWidth must be 360
assert_grep "width:\s*360" "${EVENT_LOG_QML}" "width must be 360"
assert_grep "focus:\s*true" "${EVENT_LOG_QML}" "focus must be true"
-test_case "T8.02.4" "EventLog: Background container with Theme.gray900 and Theme.borderMuted"
-assert_grep "color:\s*Theme\.gray900" "${EVENT_LOG_QML}" "Background must be Theme.gray900"
-assert_grep "border\.color:\s*Theme\.borderMuted" "${EVENT_LOG_QML}" "Border must be Theme.borderMuted"
-assert_grep "border\.width:\s*Theme\.borderWidth" "${EVENT_LOG_QML}" "Border width must be Theme.borderWidth"
+test_case "T8.02.4" "EventLog: Background container handled by Dynamic Island"
+# assert_grep "color:\s*Theme\.gray900" "${EVENT_LOG_QML}" "Background must be Theme.gray900"
+# assert_grep "border\.color:\s*Theme\.borderMuted" "${EVENT_LOG_QML}" "Border must be Theme.borderMuted"
+# assert_grep "border\.width:\s*Theme\.borderWidth" "${EVENT_LOG_QML}" "Border width must be Theme.borderWidth"
test_case "T8.02.5" "EventLog: Corner brackets decoration matching cyber aesthetic"
assert_grep "cornerBrackets" "${EVENT_LOG_QML}" "Corner brackets element must exist"
diff --git a/tests/e2e/tier1_features/test_t8_shell_wiring.sh b/tests/e2e/tier1_features/test_t8_shell_wiring.sh
index 7bd9de18..ac257c19 100755
--- a/tests/e2e/tier1_features/test_t8_shell_wiring.sh
+++ b/tests/e2e/tier1_features/test_t8_shell_wiring.sh
@@ -20,13 +20,13 @@ test_case "T8.04.2" "ShellWiring: IpcHandler contains toggleEventLog method rout
assert_grep "function\s+toggleEventLog\s*\(\)\s*:\s*void" "${SHELL_QML}" "IpcHandler must declare toggleEventLog(): void"
assert_grep "OverlayController\.toggleEventLog\(\)" "${SHELL_QML}" "toggleEventLog must invoke OverlayController.toggleEventLog()"
-test_case "T8.04.3" "ShellWiring: eventLogLoader has source desktop/surfaces/EventLog.qml"
-assert_grep "id:\s*eventLogLoader" "${SHELL_QML}" "Loader with id eventLogLoader must exist"
-assert_grep 'source:\s*"desktop/surfaces/EventLog\.qml"' "${SHELL_QML}" "eventLogLoader source must be desktop/surfaces/EventLog.qml"
+test_case "T8.04.3" "ShellWiring: eventLogLoader has source ../EventLog.qml"
+assert_grep "id:\s*eventLogEmbeddedLoader" "shell/desktop/surfaces/components/DynamicIsland.qml" "Loader with id eventLogEmbeddedLoader must exist"
+assert_grep 'source:\s*"\.\./EventLog\.qml"' "shell/desktop/surfaces/components/DynamicIsland.qml" "eventLogEmbeddedLoader source must be ../EventLog.qml"
test_case "T8.04.4" "ShellWiring: eventLogLoader visibility bound to OverlayController.Surface.EventLog"
-assert_grep "visible:\s*OverlayController\.activeSurface\s*===\s*OverlayController\.Surface\.EventLog" "${SHELL_QML}" "eventLogLoader visible condition must check Surface.EventLog"
-assert_grep "asynchronous:\s*false" "${SHELL_QML}" "eventLogLoader must be synchronous (asynchronous: false)"
+assert_grep "active:\s*root\.isEventLogOpen\s*||\s*opacity\s*>\s*0" "shell/desktop/surfaces/components/DynamicIsland.qml" "eventLogEmbeddedLoader active condition must check isEventLogOpen"
+assert_grep "opacity:\s*root\.isEventLogOpen\s*\?\s*1\.0\s*:\s*0\.0" "shell/desktop/surfaces/components/DynamicIsland.qml" "eventLogEmbeddedLoader opacity must bind to isEventLogOpen"
test_case "T8.04.5" "ShellWiring: notificationToastHost PanelWindow declared with target screen"
assert_grep "id:\s*notificationToastHost" "${SHELL_QML}" "PanelWindow notificationToastHost must exist"
From de4d83600c2e245d949bf0fbbcb48550b2c736aa Mon Sep 17 00:00:00 2001
From: reze-dev <25588579+reze-dev@users.noreply.github.com>
Date: Mon, 28 Sep 2026 21:16:46 +0530
Subject: [PATCH 42/48] fix(ui): decouple EventLog into separate PanelWindow to
fix mask resize bug
---
shell/desktop/surfaces/AmbientBar.qml | 9 +++---
shell/desktop/surfaces/EventLog.qml | 9 +++++-
.../surfaces/components/DynamicIsland.qml | 28 +++----------------
shell/shell.qml | 27 ++++++++++++++++++
.../tier1_features/test_t8_shell_wiring.sh | 12 ++++----
5 files changed, 49 insertions(+), 36 deletions(-)
diff --git a/shell/desktop/surfaces/AmbientBar.qml b/shell/desktop/surfaces/AmbientBar.qml
index 276cc4a6..3812d258 100644
--- a/shell/desktop/surfaces/AmbientBar.qml
+++ b/shell/desktop/surfaces/AmbientBar.qml
@@ -29,8 +29,8 @@ PanelWindow {
Region {
x: centerSection.x
y: centerSection.y
- width: centerSection.isEventLogOpen ? 360 : centerSection.width
- height: centerSection.isEventLogOpen ? 500 : centerSection.height
+ width: centerSection.width
+ height: centerSection.height
}
Region {
x: rightSections.x
@@ -60,7 +60,6 @@ PanelWindow {
function onWidthChanged() { root.flushWaylandMask(); }
function onHeightChanged() { root.flushWaylandMask(); }
function onXChanged() { root.flushWaylandMask(); }
- function onIsEventLogOpenChanged() { root.flushWaylandMask(); }
}
Connections {
target: rightSections
@@ -68,8 +67,8 @@ PanelWindow {
function onXChanged() { root.flushWaylandMask(); }
}
WlrLayershell.namespace: "ctos-bar"
- WlrLayershell.keyboardFocus: centerSection.isEventLogOpen ? WlrKeyboardFocus.OnDemand : WlrKeyboardFocus.None
- implicitHeight: (centerSection.isEventLogOpen || centerSection.height > Theme.barHeight) ? 506 : Theme.barHeight
+ WlrLayershell.keyboardFocus: WlrKeyboardFocus.None
+ implicitHeight: Theme.barHeight
exclusionMode: ExclusionMode.Normal
exclusiveZone: Theme.barHeight
diff --git a/shell/desktop/surfaces/EventLog.qml b/shell/desktop/surfaces/EventLog.qml
index 141ecee3..f9411742 100644
--- a/shell/desktop/surfaces/EventLog.qml
+++ b/shell/desktop/surfaces/EventLog.qml
@@ -44,7 +44,14 @@ FocusScope {
}
}
- // Background container handled by Dynamic Island
+ // Background container handled by Dynamic Island previously, now explicit
+ Rectangle {
+ anchors.fill: parent
+ color: Theme.background
+ radius: Theme.radiusMedium
+ border.color: Theme.borderMuted
+ border.width: Theme.borderWidth
+ }
// Corner Brackets decoration (cyber aesthetic)
CornerBrackets {
diff --git a/shell/desktop/surfaces/components/DynamicIsland.qml b/shell/desktop/surfaces/components/DynamicIsland.qml
index 850d2e4e..3b5ebb7e 100644
--- a/shell/desktop/surfaces/components/DynamicIsland.qml
+++ b/shell/desktop/surfaces/components/DynamicIsland.qml
@@ -22,9 +22,9 @@ Rectangle {
property int latestUrgency: 1
readonly property bool isEventLogOpen: OverlayController.activeSurface === OverlayController.Surface.EventLog
- width: isEventLogOpen ? 360 : (isExpanded ? expandedWidth : compactWidth)
+ width: isExpanded ? expandedWidth : compactWidth
implicitWidth: width
- height: isEventLogOpen ? 500 : (Theme.barHeight - 6)
+ height: Theme.barHeight - 6
implicitHeight: height
color: Theme.background
@@ -106,7 +106,7 @@ Rectangle {
RowLayout {
id: compactContent
anchors.centerIn: parent
- opacity: (root.isExpanded || root.isEventLogOpen) ? 0.0 : 1.0
+ opacity: root.isExpanded ? 0.0 : 1.0
visible: opacity > 0.0
spacing: Theme.spacingSmall
@@ -179,7 +179,7 @@ Rectangle {
anchors.fill: parent
anchors.leftMargin: Theme.paddingLarge
anchors.rightMargin: Theme.paddingLarge
- opacity: (root.isExpanded && !root.isEventLogOpen) ? 1.0 : 0.0
+ opacity: root.isExpanded ? 1.0 : 0.0
visible: opacity > 0.0
spacing: Theme.spacingSmall
@@ -240,7 +240,6 @@ Rectangle {
cursorShape: Qt.PointingHandCursor
hoverEnabled: true
acceptedButtons: Qt.LeftButton | Qt.RightButton
- enabled: !root.isEventLogOpen // Disable click to toggle when event log is fully open, as it handles its own input.
onClicked: (mouse) => {
if (mouse.button === Qt.RightButton) {
@@ -256,23 +255,4 @@ Rectangle {
}
}
}
-
- // =========================================================================
- // Event Log Container
- // =========================================================================
-
- Loader {
- id: eventLogEmbeddedLoader
- anchors.fill: parent
- active: root.isEventLogOpen || opacity > 0
- opacity: root.isEventLogOpen ? 1.0 : 0.0
- visible: opacity > 0.0
- source: "../EventLog.qml"
-
- Behavior on opacity {
- NumberAnimation {
- duration: Theme.durationFast
- }
- }
- }
}
diff --git a/shell/shell.qml b/shell/shell.qml
index e7ade314..f66af689 100644
--- a/shell/shell.qml
+++ b/shell/shell.qml
@@ -661,6 +661,33 @@ Scope {
anchors.horizontalCenter: parent.horizontalCenter
}
}
+ PanelWindow {
+ id: eventLogPopupHost
+
+ screen: overlayHost.screen
+ color: "transparent"
+ visible: OverlayController.activeSurface === OverlayController.Surface.EventLog && overlayHost.screen !== null
+ exclusionMode: ExclusionMode.Ignore
+
+ WlrLayershell.layer: WlrLayer.Overlay
+ WlrLayershell.keyboardFocus: WlrKeyboardFocus.OnDemand
+ WlrLayershell.namespace: "ctos-eventlog-popup"
+
+ anchors {
+ top: true
+
+ }
+ margins {
+ top: Settings.barHeight + Theme.spacingMedium
+ }
+
+ implicitWidth: eventLogPopup.implicitWidth
+ implicitHeight: eventLogPopup.implicitHeight
+
+ EventLog {
+ id: eventLogPopup
+ }
+ }
PanelWindow {
id: calendarBackdropHost
diff --git a/tests/e2e/tier1_features/test_t8_shell_wiring.sh b/tests/e2e/tier1_features/test_t8_shell_wiring.sh
index ac257c19..7e21ce02 100755
--- a/tests/e2e/tier1_features/test_t8_shell_wiring.sh
+++ b/tests/e2e/tier1_features/test_t8_shell_wiring.sh
@@ -20,13 +20,13 @@ test_case "T8.04.2" "ShellWiring: IpcHandler contains toggleEventLog method rout
assert_grep "function\s+toggleEventLog\s*\(\)\s*:\s*void" "${SHELL_QML}" "IpcHandler must declare toggleEventLog(): void"
assert_grep "OverlayController\.toggleEventLog\(\)" "${SHELL_QML}" "toggleEventLog must invoke OverlayController.toggleEventLog()"
-test_case "T8.04.3" "ShellWiring: eventLogLoader has source ../EventLog.qml"
-assert_grep "id:\s*eventLogEmbeddedLoader" "shell/desktop/surfaces/components/DynamicIsland.qml" "Loader with id eventLogEmbeddedLoader must exist"
-assert_grep 'source:\s*"\.\./EventLog\.qml"' "shell/desktop/surfaces/components/DynamicIsland.qml" "eventLogEmbeddedLoader source must be ../EventLog.qml"
+test_case "T8.04.3" "ShellWiring: eventLogPopupHost PanelWindow declared with target screen"
+assert_grep "id:\s*eventLogPopupHost" "${SHELL_QML}" "PanelWindow eventLogPopupHost must exist"
+assert_grep "screen:\s*overlayHost\.screen" "${SHELL_QML}" "eventLogPopupHost must target overlayHost screen"
-test_case "T8.04.4" "ShellWiring: eventLogLoader visibility bound to OverlayController.Surface.EventLog"
-assert_grep "active:\s*root\.isEventLogOpen\s*||\s*opacity\s*>\s*0" "shell/desktop/surfaces/components/DynamicIsland.qml" "eventLogEmbeddedLoader active condition must check isEventLogOpen"
-assert_grep "opacity:\s*root\.isEventLogOpen\s*\?\s*1\.0\s*:\s*0\.0" "shell/desktop/surfaces/components/DynamicIsland.qml" "eventLogEmbeddedLoader opacity must bind to isEventLogOpen"
+test_case "T8.04.4" "ShellWiring: eventLogPopupHost visibility bound to OverlayController.Surface.EventLog"
+assert_grep "visible:\s*OverlayController\.activeSurface\s*===\s*OverlayController\.Surface\.EventLog" "${SHELL_QML}" "eventLogPopupHost visible must check activeSurface"
+assert_grep "EventLog\s*\{" "${SHELL_QML}" "eventLogPopupHost must instantiate EventLog"
test_case "T8.04.5" "ShellWiring: notificationToastHost PanelWindow declared with target screen"
assert_grep "id:\s*notificationToastHost" "${SHELL_QML}" "PanelWindow notificationToastHost must exist"
From e1c7a2d27bf187c287d5b477da7fd662a7a31138 Mon Sep 17 00:00:00 2001
From: reze-dev <25588579+reze-dev@users.noreply.github.com>
Date: Tue, 29 Sep 2026 00:56:33 +0530
Subject: [PATCH 43/48] feat(ui): dynamically enlarge dynamic island into
command center event log
- Add three-state geometry model to DynamicIsland ('compact', 'notification', 'eventLog')
- Dynamically enlarge island from 120x34 to 360x500 on click
- Embed full EventLog command center inside DynamicIsland with opacity cross-fade
- Dynamically scale AmbientBar implicitHeight to track island expansion
- Remove standalone eventLogPopupHost PanelWindow in shell.qml
---
shell/desktop/surfaces/AmbientBar.qml | 4 +-
.../surfaces/components/DynamicIsland.qml | 619 +++++++++++++++++-
shell/shell.qml | 55 +-
3 files changed, 640 insertions(+), 38 deletions(-)
diff --git a/shell/desktop/surfaces/AmbientBar.qml b/shell/desktop/surfaces/AmbientBar.qml
index 3812d258..6a63eb40 100644
--- a/shell/desktop/surfaces/AmbientBar.qml
+++ b/shell/desktop/surfaces/AmbientBar.qml
@@ -67,8 +67,8 @@ PanelWindow {
function onXChanged() { root.flushWaylandMask(); }
}
WlrLayershell.namespace: "ctos-bar"
- WlrLayershell.keyboardFocus: WlrKeyboardFocus.None
- implicitHeight: Theme.barHeight
+ WlrLayershell.keyboardFocus: OverlayController.activeSurface === OverlayController.Surface.EventLog ? WlrKeyboardFocus.OnDemand : WlrKeyboardFocus.None
+ implicitHeight: centerSection.height > Theme.barHeight ? centerSection.y + centerSection.height + 8 : Theme.barHeight
exclusionMode: ExclusionMode.Normal
exclusiveZone: Theme.barHeight
diff --git a/shell/desktop/surfaces/components/DynamicIsland.qml b/shell/desktop/surfaces/components/DynamicIsland.qml
index 3b5ebb7e..90688078 100644
--- a/shell/desktop/surfaces/components/DynamicIsland.qml
+++ b/shell/desktop/surfaces/components/DynamicIsland.qml
@@ -5,6 +5,7 @@ import QtQuick.Layouts
import Quickshell
import "../../core"
import "../../services"
+import "../widgets"
Rectangle {
id: root
@@ -15,21 +16,62 @@ Rectangle {
readonly property real compactWidth: 120
readonly property real expandedWidth: 300
+ readonly property real eventLogWidth: 360
+ readonly property real eventLogHeight: 500
+ // Three-state property: "compact", "notification", "eventLog"
+ property string state: "compact"
+
+ // Writable isExpanded for backward compatibility with tests.
+ // Tests directly set isExpanded = true/false, so we keep it writable
+ // and sync it with the state property.
property bool isExpanded: false
+
property string latestAppName: ""
property string latestSummary: ""
property int latestUrgency: 1
readonly property bool isEventLogOpen: OverlayController.activeSurface === OverlayController.Surface.EventLog
- width: isExpanded ? expandedWidth : compactWidth
+ // Sync isExpanded writes → state
+ onIsExpandedChanged: {
+ if (root.isExpanded && root.state === "compact") {
+ root.state = "notification";
+ } else if (!root.isExpanded && root.state === "notification") {
+ root.state = "compact";
+ }
+ }
+
+ // Sync state → isExpanded (for notification state changes from showNotification)
+ onStateChanged: {
+ if (root.state === "notification" && !root.isExpanded) {
+ root.isExpanded = true;
+ } else if (root.state === "compact" && root.isExpanded) {
+ root.isExpanded = false;
+ }
+ }
+
+ // Centralized layout function per dynamic-radial-geometry skill:
+ // Synchronously compute width/height based on state to avoid desync.
+ function _computeLayout(): var {
+ if (root.state === "eventLog") {
+ return { w: root.eventLogWidth, h: root.eventLogHeight };
+ } else if (root.state === "notification") {
+ return { w: root.expandedWidth, h: Theme.barHeight - 6 };
+ } else {
+ return { w: root.compactWidth, h: Theme.barHeight - 6 };
+ }
+ }
+
+ property var _layout: _computeLayout()
+
+ width: _layout.w
implicitWidth: width
- height: Theme.barHeight - 6
+ height: _layout.h
implicitHeight: height
color: Theme.background
radius: Theme.radiusMedium
- border.color: mouseArea.containsMouse ? Theme.accent : Theme.borderMuted
+ border.color: mouseArea.containsMouse && root.state !== "eventLog" ? Theme.accent : Theme.borderMuted
border.width: Theme.borderWidth
clip: true
@@ -51,7 +93,7 @@ Rectangle {
}
}
- // Auto-collapse after 4 seconds
+ // Auto-collapse after 4 seconds (only for notification state)
Timer {
id: collapseTimer
interval: 4000
@@ -99,6 +141,39 @@ Rectangle {
}
}
+ // =========================================================================
+ // OverlayController Connection — react to external close
+ // =========================================================================
+
+ Connections {
+ target: OverlayController
+
+ function onOverlayClosed(previousSurface: int): void {
+ if (previousSurface === OverlayController.Surface.EventLog && root.state === "eventLog") {
+ root.state = "compact";
+ }
+ }
+
+ function onOverlayOpened(surface: int): void {
+ if (surface === OverlayController.Surface.EventLog && root.state === "eventLog") {
+ root.forceActiveFocus();
+ }
+ }
+ }
+
+ // =========================================================================
+ // Keyboard Handling (Escape to close eventLog)
+ // =========================================================================
+
+ Keys.onEscapePressed: function (event) {
+ if (root.state === "eventLog") {
+ OverlayController.close();
+ if (event) {
+ event.accepted = true;
+ }
+ }
+ }
+
// =========================================================================
// Compact View
// =========================================================================
@@ -106,7 +181,7 @@ Rectangle {
RowLayout {
id: compactContent
anchors.centerIn: parent
- opacity: root.isExpanded ? 0.0 : 1.0
+ opacity: root.state === "compact" ? 1.0 : 0.0
visible: opacity > 0.0
spacing: Theme.spacingSmall
@@ -179,7 +254,7 @@ Rectangle {
anchors.fill: parent
anchors.leftMargin: Theme.paddingLarge
anchors.rightMargin: Theme.paddingLarge
- opacity: root.isExpanded ? 1.0 : 0.0
+ opacity: root.state === "notification" ? 1.0 : 0.0
visible: opacity > 0.0
spacing: Theme.spacingSmall
@@ -230,6 +305,518 @@ Rectangle {
}
}
+ // =========================================================================
+ // Event Log View (Embedded — morphs from DynamicIsland)
+ // =========================================================================
+
+ property int eventLogCurrentTab: 0
+
+ // Corner Brackets decoration (cyber aesthetic) — visible only in eventLog state
+ CornerBrackets {
+ id: cornerBrackets
+ bracketColor: Theme.acidGreen
+ margin: Theme.cornerBracketMargin
+ armLength: Theme.cornerBracketArmLength
+ thickness: Theme.cornerBracketThickness
+ z: 10
+ opacity: root.state === "eventLog" ? 1.0 : 0.0
+ visible: opacity > 0.0
+
+ Behavior on opacity {
+ NumberAnimation {
+ duration: Settings.reducedMotion ? 0 : Theme.durationFast
+ }
+ }
+ }
+
+ // Event Log Content
+ Item {
+ id: eventLogContent
+ anchors.fill: parent
+ opacity: root.state === "eventLog" ? 1.0 : 0.0
+ visible: opacity > 0.0
+
+ Behavior on opacity {
+ NumberAnimation {
+ duration: Settings.reducedMotion ? 0 : Theme.durationFast
+ }
+ }
+
+ // Inside Click Consumer — prevent clicks from falling through
+ MouseArea {
+ id: eventLogInsideClickConsumer
+ anchors.fill: parent
+ hoverEnabled: true
+ preventStealing: true
+ onClicked: function (mouse) {
+ mouse.accepted = true;
+ }
+ }
+
+ // Main Content Column
+ ColumnLayout {
+ id: eventLogMainLayout
+ anchors.fill: parent
+ anchors.margins: Theme.paddingXl
+ spacing: Theme.spacingMedium
+
+ // Header Row
+ RowLayout {
+ Layout.fillWidth: true
+ spacing: Theme.spacingMedium
+
+ Rectangle {
+ Layout.preferredHeight: 8
+ Layout.preferredWidth: 8
+ color: Theme.accent
+ radius: 4
+ }
+
+ Text {
+ Layout.fillWidth: true
+ color: Theme.accent
+ font.family: Theme.fontFamilyMonospace
+ font.pixelSize: Theme.fontSizeSegment
+ font.weight: Theme.fontWeightBold
+ text: "// EVENT LOG"
+ }
+
+ // DND Toggle Button
+ Rectangle {
+ id: eventLogDndButton
+ Layout.preferredHeight: 24
+ Layout.preferredWidth: eventLogDndLabel.implicitWidth + Theme.paddingMedium * 2
+ border.color: NotificationService.doNotDisturb ? Theme.warningRed : (eventLogDndMouseArea.containsMouse ? Theme.accent : Theme.borderMuted)
+ border.width: Theme.borderWidth
+ color: NotificationService.doNotDisturb ? Theme.surfaceSelected : (eventLogDndMouseArea.containsMouse ? Theme.surfaceHover : "transparent")
+ radius: Theme.radiusSmall
+
+ Text {
+ id: eventLogDndLabel
+ anchors.centerIn: parent
+ color: NotificationService.doNotDisturb ? Theme.warningRed : (eventLogDndMouseArea.containsMouse ? Theme.accent : Theme.textSecondary)
+ font.family: Theme.fontFamilyMonospace
+ font.pixelSize: Theme.fontSizeCaption
+ font.weight: Theme.fontWeightBold
+ text: NotificationService.doNotDisturb ? "[DND]" : "[DND OFF]"
+ }
+
+ MouseArea {
+ id: eventLogDndMouseArea
+ anchors.fill: parent
+ cursorShape: Qt.PointingHandCursor
+ hoverEnabled: true
+ onClicked: NotificationService.toggleDnd()
+ }
+ }
+
+ // Close Button
+ Rectangle {
+ id: eventLogCloseButton
+ Layout.preferredHeight: 24
+ Layout.preferredWidth: 24
+ border.color: eventLogCloseMouseArea.containsMouse ? Theme.accent : Theme.borderMuted
+ border.width: Theme.borderWidth
+ color: eventLogCloseMouseArea.containsMouse ? Theme.surfaceHover : "transparent"
+ radius: Theme.radiusSmall
+
+ Text {
+ anchors.centerIn: parent
+ color: eventLogCloseMouseArea.containsMouse ? Theme.accent : Theme.textSecondary
+ font.family: Theme.fontFamilyMonospace
+ font.pixelSize: Theme.fontSizeCaption
+ font.weight: Theme.fontWeightBold
+ text: "x"
+ }
+
+ MouseArea {
+ id: eventLogCloseMouseArea
+ anchors.fill: parent
+ cursorShape: Qt.PointingHandCursor
+ hoverEnabled: true
+ onClicked: OverlayController.close()
+ }
+ }
+ }
+
+ // Header Divider & Tabs
+ ColumnLayout {
+ Layout.fillWidth: true
+ spacing: 0
+
+ RowLayout {
+ Layout.fillWidth: true
+ spacing: Theme.spacingMedium
+
+ Rectangle {
+ id: eventLogTab0
+ Layout.preferredHeight: 24
+ Layout.preferredWidth: eventLogTab0Text.implicitWidth + Theme.paddingMedium * 2
+ color: root.eventLogCurrentTab === 0 ? Theme.surfaceSelected : (eventLogTab0MouseArea.containsMouse ? Theme.surfaceHover : "transparent")
+ border.color: root.eventLogCurrentTab === 0 ? Theme.accent : Theme.borderMuted
+ border.width: Theme.borderWidth
+ radius: Theme.radiusSmall
+
+ Text {
+ id: eventLogTab0Text
+ anchors.centerIn: parent
+ color: root.eventLogCurrentTab === 0 ? Theme.accent : Theme.textSecondary
+ font.family: Theme.fontFamilyMonospace
+ font.pixelSize: Theme.fontSizeCaption
+ font.weight: Theme.fontWeightBold
+ text: "NOTIFICATIONS"
+ }
+
+ MouseArea {
+ id: eventLogTab0MouseArea
+ anchors.fill: parent
+ hoverEnabled: true
+ cursorShape: Qt.PointingHandCursor
+ onClicked: root.eventLogCurrentTab = 0
+ }
+ }
+
+ Rectangle {
+ id: eventLogTab1
+ Layout.preferredHeight: 24
+ Layout.preferredWidth: eventLogTab1Text.implicitWidth + Theme.paddingMedium * 2
+ color: root.eventLogCurrentTab === 1 ? Theme.surfaceSelected : (eventLogTab1MouseArea.containsMouse ? Theme.surfaceHover : "transparent")
+ border.color: root.eventLogCurrentTab === 1 ? Theme.accent : Theme.borderMuted
+ border.width: Theme.borderWidth
+ radius: Theme.radiusSmall
+
+ Text {
+ id: eventLogTab1Text
+ anchors.centerIn: parent
+ color: root.eventLogCurrentTab === 1 ? Theme.accent : Theme.textSecondary
+ font.family: Theme.fontFamilyMonospace
+ font.pixelSize: Theme.fontSizeCaption
+ font.weight: Theme.fontWeightBold
+ text: "SYSTEM LOGS"
+ }
+
+ MouseArea {
+ id: eventLogTab1MouseArea
+ anchors.fill: parent
+ hoverEnabled: true
+ cursorShape: Qt.PointingHandCursor
+ onClicked: root.eventLogCurrentTab = 1
+ }
+ }
+ }
+
+ Item {
+ Layout.preferredHeight: Theme.spacingMedium
+ }
+
+ Rectangle {
+ Layout.fillWidth: true
+ Layout.preferredHeight: Theme.borderWidth
+ color: Theme.borderMuted
+ }
+ }
+
+ // Body Content Area (ListView or Empty State)
+ Item {
+ id: eventLogContentContainer
+ Layout.fillWidth: true
+ Layout.fillHeight: true
+ visible: root.eventLogCurrentTab === 0
+
+ // Notification History ListView
+ ListView {
+ id: eventLogHistoryListView
+ anchors.fill: parent
+ clip: true
+ boundsBehavior: Flickable.StopAtBounds
+ spacing: Theme.spacingSmall
+ model: NotificationService.history
+ visible: NotificationService.history.count > 0
+
+ delegate: Rectangle {
+ id: eventLogCardItem
+ required property int index
+ required property var notifId
+ required property string appName
+ required property string summary
+ required property string body
+ required property int urgency
+ required property string timestamp
+
+ width: eventLogHistoryListView.width
+ implicitHeight: eventLogCardLayout.implicitHeight + Theme.paddingMedium * 2
+ color: Theme.gray800
+ border.color: Theme.gray700
+ border.width: Theme.borderWidth
+ radius: Theme.radiusSmall
+
+ readonly property color urgencyColor: {
+ if (eventLogCardItem.urgency === 2) {
+ return Theme.warningRed;
+ }
+ if (eventLogCardItem.urgency === 0) {
+ return Theme.textMuted;
+ }
+ return Theme.acidGreen;
+ }
+
+ // Urgency accent stripe on left edge
+ Rectangle {
+ id: eventLogUrgencyStripe
+ anchors.bottom: parent.bottom
+ anchors.left: parent.left
+ anchors.top: parent.top
+ color: eventLogCardItem.urgencyColor
+ radius: Theme.radiusSmall
+ width: 3
+ }
+
+ ColumnLayout {
+ id: eventLogCardLayout
+ anchors.left: eventLogUrgencyStripe.right
+ anchors.right: parent.right
+ anchors.top: parent.top
+ anchors.margins: Theme.paddingMedium
+ spacing: Theme.spacingSmall
+
+ // Header line: Urgency Dot, App Name (left), Timestamp (right)
+ RowLayout {
+ Layout.fillWidth: true
+ spacing: Theme.spacingSmall
+
+ Rectangle {
+ Layout.preferredHeight: 6
+ Layout.preferredWidth: 6
+ color: eventLogCardItem.urgencyColor
+ radius: 3
+ }
+
+ Text {
+ Layout.fillWidth: true
+ color: Theme.textSecondary
+ elide: Text.ElideRight
+ font.family: Theme.fontFamilyMonospace
+ font.pixelSize: Theme.fontSizeCaption
+ font.weight: Theme.fontWeightMedium
+ text: eventLogCardItem.appName.toUpperCase()
+ }
+
+ Text {
+ color: Theme.textMuted
+ font.family: Theme.fontFamilyMonospace
+ font.pixelSize: Theme.fontSizeCaption
+ text: eventLogCardItem.timestamp
+ }
+ }
+
+ // Summary text
+ Text {
+ Layout.fillWidth: true
+ color: Theme.textPrimary
+ elide: Text.ElideRight
+ font.family: Theme.fontFamilyMonospace
+ font.pixelSize: Theme.fontSizeSmall
+ font.weight: Theme.fontWeightBold
+ text: eventLogCardItem.summary
+ }
+
+ // Body text (word wrap, max 3 lines, elide)
+ Text {
+ Layout.fillWidth: true
+ color: Theme.textSecondary
+ elide: Text.ElideRight
+ font.family: Theme.fontFamilyMonospace
+ font.pixelSize: Theme.fontSizeCaption
+ maximumLineCount: 3
+ text: eventLogCardItem.body
+ visible: eventLogCardItem.body !== ""
+ wrapMode: Text.WordWrap
+ }
+
+ // Dismiss action button
+ RowLayout {
+ Layout.fillWidth: true
+
+ Item {
+ Layout.fillWidth: true
+ }
+
+ Rectangle {
+ id: eventLogDismissBtn
+ Layout.preferredHeight: 20
+ Layout.preferredWidth: eventLogDismissText.implicitWidth + Theme.paddingSmall * 2
+ border.color: eventLogDismissMouseArea.containsMouse ? Theme.accent : Theme.borderMuted
+ border.width: Theme.borderWidth
+ color: eventLogDismissMouseArea.containsMouse ? Theme.surfaceHover : "transparent"
+ radius: Theme.radiusSmall
+
+ Text {
+ id: eventLogDismissText
+ anchors.centerIn: parent
+ color: eventLogDismissMouseArea.containsMouse ? Theme.accent : Theme.textSecondary
+ font.family: Theme.fontFamilyMonospace
+ font.pixelSize: Theme.fontSizeCaption
+ font.weight: Theme.fontWeightMedium
+ text: "[DISMISS]"
+ }
+
+ MouseArea {
+ id: eventLogDismissMouseArea
+ anchors.fill: parent
+ cursorShape: Qt.PointingHandCursor
+ hoverEnabled: true
+ onClicked: NotificationService.dismissHistoryItem(eventLogCardItem.index)
+ }
+ }
+ }
+ }
+ }
+ }
+
+ // Scrollbar track & thumb
+ Rectangle {
+ id: eventLogScrollTrack
+ anchors.bottom: parent.bottom
+ anchors.right: parent.right
+ anchors.top: parent.top
+ color: Theme.gray800
+ radius: 2
+ visible: eventLogHistoryListView.visible && (eventLogHistoryListView.height < eventLogHistoryListView.contentHeight)
+ width: 4
+
+ Rectangle {
+ id: eventLogScrollThumb
+ color: Theme.acidGreen
+ height: Math.max(16, eventLogHistoryListView.visibleArea.heightRatio * eventLogHistoryListView.height)
+ radius: 2
+ width: 4
+ y: eventLogHistoryListView.visibleArea.yPosition * eventLogHistoryListView.height
+ }
+ }
+
+ // Empty state container
+ Item {
+ id: eventLogEmptyState
+ anchors.fill: parent
+ visible: NotificationService.history.count === 0
+
+ ColumnLayout {
+ anchors.centerIn: parent
+ spacing: Theme.spacingMedium
+
+ Rectangle {
+ Layout.alignment: Qt.AlignHCenter
+ Layout.preferredHeight: 8
+ Layout.preferredWidth: 8
+ color: Theme.textMuted
+ radius: 4
+ }
+
+ Text {
+ Layout.alignment: Qt.AlignHCenter
+ color: Theme.textMuted
+ font.family: Theme.fontFamilyMonospace
+ font.pixelSize: Theme.fontSizeSmall
+ font.weight: Theme.fontWeightMedium
+ text: "NO NOTIFICATIONS // STANDBY"
+ }
+ }
+ }
+ }
+
+ // System Logs Content Area
+ Item {
+ id: eventLogSystemLogsContainer
+ Layout.fillWidth: true
+ Layout.fillHeight: true
+ visible: root.eventLogCurrentTab === 1
+
+ ColumnLayout {
+ anchors.centerIn: parent
+ spacing: Theme.spacingMedium
+
+ Rectangle {
+ Layout.alignment: Qt.AlignHCenter
+ Layout.preferredHeight: 8
+ Layout.preferredWidth: 8
+ color: Theme.textMuted
+ radius: 4
+ }
+
+ Text {
+ Layout.alignment: Qt.AlignHCenter
+ color: Theme.textMuted
+ font.family: Theme.fontFamilyMonospace
+ font.pixelSize: Theme.fontSizeSmall
+ font.weight: Theme.fontWeightMedium
+ text: "SYSTEM LOGS // UNAVAILABLE"
+ }
+ }
+ }
+
+ // Footer Divider
+ Rectangle {
+ Layout.fillWidth: true
+ Layout.preferredHeight: Theme.borderWidth
+ color: Theme.borderMuted
+ visible: root.eventLogCurrentTab === 0
+ }
+
+ // Footer Action Row
+ RowLayout {
+ Layout.fillWidth: true
+ spacing: Theme.spacingMedium
+ visible: root.eventLogCurrentTab === 0
+
+ Text {
+ Layout.fillWidth: true
+ color: Theme.textSecondary
+ font.family: Theme.fontFamilyMonospace
+ font.pixelSize: Theme.fontSizeCaption
+ text: NotificationService.history.count > 0
+ ? NotificationService.history.count + " LOGGED"
+ : "0 LOGGED"
+ }
+
+ Rectangle {
+ id: eventLogClearAllBtn
+ Layout.preferredHeight: 26
+ Layout.preferredWidth: eventLogClearAllText.implicitWidth + Theme.paddingMedium * 2
+ border.color: NotificationService.history.count === 0
+ ? Theme.borderMuted
+ : (eventLogClearMouseArea.containsMouse ? Theme.destructive : Theme.borderMuted)
+ border.width: Theme.borderWidth
+ color: NotificationService.history.count === 0
+ ? "transparent"
+ : (eventLogClearMouseArea.containsMouse ? Theme.surfaceHover : "transparent")
+ opacity: NotificationService.history.count === 0 ? 0.4 : 1.0
+ radius: Theme.radiusSmall
+
+ Text {
+ id: eventLogClearAllText
+ anchors.centerIn: parent
+ color: NotificationService.history.count === 0
+ ? Theme.textMuted
+ : (eventLogClearMouseArea.containsMouse ? Theme.destructive : Theme.textPrimary)
+ font.family: Theme.fontFamilyMonospace
+ font.pixelSize: Theme.fontSizeCaption
+ font.weight: Theme.fontWeightBold
+ text: "[CLEAR ALL]"
+ }
+
+ MouseArea {
+ id: eventLogClearMouseArea
+ anchors.fill: parent
+ cursorShape: NotificationService.history.count > 0 ? Qt.PointingHandCursor : Qt.ArrowCursor
+ enabled: NotificationService.history.count > 0
+ hoverEnabled: NotificationService.history.count > 0
+ onClicked: NotificationService.clearAll()
+ }
+ }
+ }
+ }
+ }
+
// =========================================================================
// Mouse Interaction
// =========================================================================
@@ -241,16 +828,30 @@ Rectangle {
hoverEnabled: true
acceptedButtons: Qt.LeftButton | Qt.RightButton
+ // Only handle clicks in compact/notification states.
+ // In eventLog state, eventLogInsideClickConsumer takes precedence
+ // except for this top-level area which we still need for right-click DND.
onClicked: (mouse) => {
if (mouse.button === Qt.RightButton) {
NotificationService.toggleDnd();
} else {
- if (root.isExpanded) {
- root.isExpanded = false;
+ if (root.state === "eventLog") {
+ // Toggle off: close the event log
+ OverlayController.close();
+ } else if (root.state === "notification") {
+ // Collapse notification banner and open event log
collapseTimer.stop();
+ root.isExpanded = false;
+ root.state = "eventLog";
OverlayController.openEventLog();
} else {
- OverlayController.toggleEventLog();
+ // Compact state: toggle event log
+ if (root.isEventLogOpen) {
+ OverlayController.close();
+ } else {
+ root.state = "eventLog";
+ OverlayController.openEventLog();
+ }
}
}
}
diff --git a/shell/shell.qml b/shell/shell.qml
index f66af689..e6f45858 100644
--- a/shell/shell.qml
+++ b/shell/shell.qml
@@ -661,33 +661,34 @@ Scope {
anchors.horizontalCenter: parent.horizontalCenter
}
}
- PanelWindow {
- id: eventLogPopupHost
-
- screen: overlayHost.screen
- color: "transparent"
- visible: OverlayController.activeSurface === OverlayController.Surface.EventLog && overlayHost.screen !== null
- exclusionMode: ExclusionMode.Ignore
-
- WlrLayershell.layer: WlrLayer.Overlay
- WlrLayershell.keyboardFocus: WlrKeyboardFocus.OnDemand
- WlrLayershell.namespace: "ctos-eventlog-popup"
-
- anchors {
- top: true
-
- }
- margins {
- top: Settings.barHeight + Theme.spacingMedium
- }
-
- implicitWidth: eventLogPopup.implicitWidth
- implicitHeight: eventLogPopup.implicitHeight
-
- EventLog {
- id: eventLogPopup
- }
- }
+ // EventLog is now embedded in DynamicIsland — standalone popup removed
+ // PanelWindow {
+ // id: eventLogPopupHost
+ //
+ // screen: overlayHost.screen
+ // color: "transparent"
+ // visible: OverlayController.activeSurface === OverlayController.Surface.EventLog && overlayHost.screen !== null
+ // exclusionMode: ExclusionMode.Ignore
+ //
+ // WlrLayershell.layer: WlrLayer.Overlay
+ // WlrLayershell.keyboardFocus: WlrKeyboardFocus.OnDemand
+ // WlrLayershell.namespace: "ctos-eventlog-popup"
+ //
+ // anchors {
+ // top: true
+ //
+ // }
+ // margins {
+ // top: Settings.barHeight + Theme.spacingMedium
+ // }
+ //
+ // implicitWidth: eventLogPopup.implicitWidth
+ // implicitHeight: eventLogPopup.implicitHeight
+ //
+ // EventLog {
+ // id: eventLogPopup
+ // }
+ // }
PanelWindow {
id: calendarBackdropHost
From 099a2957cb034008d3e045148878c55424eb3092 Mon Sep 17 00:00:00 2001
From: reze-dev <25588579+reze-dev@users.noreply.github.com>
Date: Tue, 29 Sep 2026 01:36:59 +0530
Subject: [PATCH 44/48] feat(ui): implement Approach A hybrid takeover for
dynamic island morph
- Fix lag and clickability by delegating morph to WlrLayer.Overlay eventLogPopupHost
- Keep AmbientBar at fixed 40px height with static Wayland mask
- Use fixed transparent canvas (360x508) in shell.qml to prevent Wayland surface resizing
- Add GPU-accelerated morphing panel in EventLog.qml (120x34 to 360x500) with content cross-fading
- Seamlessly fade DynamicIsland in the bar when overlay is active
- Document Approach A and B in shell/docs/design/dynamic-island-event-log-morph.md
---
shell/desktop/surfaces/AmbientBar.qml | 4 +-
shell/desktop/surfaces/EventLog.qml | 756 ++++++++++--------
.../surfaces/components/DynamicIsland.qml | 607 +-------------
.../design/dynamic-island-event-log-morph.md | 93 +++
shell/shell.qml | 56 +-
5 files changed, 549 insertions(+), 967 deletions(-)
create mode 100644 shell/docs/design/dynamic-island-event-log-morph.md
diff --git a/shell/desktop/surfaces/AmbientBar.qml b/shell/desktop/surfaces/AmbientBar.qml
index 6a63eb40..3812d258 100644
--- a/shell/desktop/surfaces/AmbientBar.qml
+++ b/shell/desktop/surfaces/AmbientBar.qml
@@ -67,8 +67,8 @@ PanelWindow {
function onXChanged() { root.flushWaylandMask(); }
}
WlrLayershell.namespace: "ctos-bar"
- WlrLayershell.keyboardFocus: OverlayController.activeSurface === OverlayController.Surface.EventLog ? WlrKeyboardFocus.OnDemand : WlrKeyboardFocus.None
- implicitHeight: centerSection.height > Theme.barHeight ? centerSection.y + centerSection.height + 8 : Theme.barHeight
+ WlrLayershell.keyboardFocus: WlrKeyboardFocus.None
+ implicitHeight: Theme.barHeight
exclusionMode: ExclusionMode.Normal
exclusiveZone: Theme.barHeight
diff --git a/shell/desktop/surfaces/EventLog.qml b/shell/desktop/surfaces/EventLog.qml
index f9411742..924968cf 100644
--- a/shell/desktop/surfaces/EventLog.qml
+++ b/shell/desktop/surfaces/EventLog.qml
@@ -18,6 +18,7 @@ FocusScope {
focus: true
property int currentTab: 0
+ readonly property bool isOpen: OverlayController.activeSurface === OverlayController.Surface.EventLog
// Keyboard navigation focus & Tiered Escape trapping
Keys.onEscapePressed: function (event) {
@@ -44,382 +45,461 @@ FocusScope {
}
}
- // Background container handled by Dynamic Island previously, now explicit
- Rectangle {
+ // Dismiss area covering the outer canvas outside the animated panel
+ MouseArea {
+ id: canvasDismissArea
anchors.fill: parent
+ onClicked: OverlayController.close()
+ }
+
+ // Animated Visual Panel Container (morphs from Dynamic Island 120x34 to 360x500)
+ Rectangle {
+ id: panelContainer
+ anchors.horizontalCenter: parent.horizontalCenter
+ anchors.top: parent.top
+ width: root.isOpen ? 360 : 120
+ height: root.isOpen ? 500 : 34
+ radius: root.isOpen ? Theme.radiusMedium : Theme.radiusPill
color: Theme.background
- radius: Theme.radiusMedium
border.color: Theme.borderMuted
border.width: Theme.borderWidth
- }
-
- // Corner Brackets decoration (cyber aesthetic)
- CornerBrackets {
- id: cornerBrackets
- bracketColor: Theme.acidGreen
- margin: Theme.cornerBracketMargin
- armLength: Theme.cornerBracketArmLength
- thickness: Theme.cornerBracketThickness
- z: 10
- }
+ clip: true
- // Inside Click Consumer
- MouseArea {
- id: insideClickConsumer
- anchors.fill: parent
- hoverEnabled: true
- preventStealing: true
- onClicked: function (mouse) {
- mouse.accepted = true;
+ Behavior on width {
+ NumberAnimation {
+ duration: Settings.reducedMotion ? 0 : Theme.durationSlow
+ easing.type: Easing.InOutQuad
+ }
}
- }
-
- // Main Content Column
- ColumnLayout {
- id: mainLayout
- anchors.fill: parent
- anchors.margins: Theme.paddingXl
- spacing: Theme.spacingMedium
- // Header Row
- RowLayout {
- Layout.fillWidth: true
- spacing: Theme.spacingMedium
-
- Rectangle {
- Layout.preferredHeight: 8
- Layout.preferredWidth: 8
- color: Theme.accent
- radius: 4
+ Behavior on height {
+ NumberAnimation {
+ duration: Settings.reducedMotion ? 0 : Theme.durationSlow
+ easing.type: Easing.InOutQuad
}
+ }
- Text {
- Layout.fillWidth: true
- color: Theme.accent
- font.family: Theme.fontFamilyMonospace
- font.pixelSize: Theme.fontSizeSegment
- font.weight: Theme.fontWeightBold
- text: "// EVENT LOG"
+ Behavior on radius {
+ NumberAnimation {
+ duration: Settings.reducedMotion ? 0 : Theme.durationSlow
+ easing.type: Easing.InOutQuad
}
+ }
- // DND Toggle Button
- Rectangle {
- id: dndButton
- Layout.preferredHeight: 24
- Layout.preferredWidth: dndLabel.implicitWidth + Theme.paddingMedium * 2
- border.color: NotificationService.doNotDisturb ? Theme.warningRed : (dndMouseArea.containsMouse ? Theme.accent : Theme.borderMuted)
- border.width: Theme.borderWidth
- color: NotificationService.doNotDisturb ? Theme.surfaceSelected : (dndMouseArea.containsMouse ? Theme.surfaceHover : "transparent")
- radius: Theme.radiusSmall
-
- Text {
- id: dndLabel
- anchors.centerIn: parent
- color: NotificationService.doNotDisturb ? Theme.warningRed : (dndMouseArea.containsMouse ? Theme.accent : Theme.textSecondary)
- font.family: Theme.fontFamilyMonospace
- font.pixelSize: Theme.fontSizeCaption
- font.weight: Theme.fontWeightBold
- text: NotificationService.doNotDisturb ? "[DND]" : "[DND OFF]"
- }
-
- MouseArea {
- id: dndMouseArea
- anchors.fill: parent
- cursorShape: Qt.PointingHandCursor
- hoverEnabled: true
- onClicked: NotificationService.toggleDnd()
- }
+ // Inside Click Consumer
+ MouseArea {
+ id: insideClickConsumer
+ anchors.fill: parent
+ hoverEnabled: true
+ preventStealing: true
+ onClicked: function (mouse) {
+ mouse.accepted = true;
}
+ }
- // Close Button
- Rectangle {
- id: closeButton
- Layout.preferredHeight: 24
- Layout.preferredWidth: 24
- border.color: closeMouseArea.containsMouse ? Theme.accent : Theme.borderMuted
- border.width: Theme.borderWidth
- color: closeMouseArea.containsMouse ? Theme.surfaceHover : "transparent"
- radius: Theme.radiusSmall
-
- Text {
- anchors.centerIn: parent
- color: closeMouseArea.containsMouse ? Theme.accent : Theme.textSecondary
- font.family: Theme.fontFamilyMonospace
- font.pixelSize: Theme.fontSizeCaption
- font.weight: Theme.fontWeightBold
- text: "x"
- }
-
- MouseArea {
- id: closeMouseArea
- anchors.fill: parent
- cursorShape: Qt.PointingHandCursor
- hoverEnabled: true
- onClicked: OverlayController.close()
+ // Corner Brackets decoration (cyber aesthetic)
+ CornerBrackets {
+ id: cornerBrackets
+ bracketColor: Theme.acidGreen
+ margin: Theme.cornerBracketMargin
+ armLength: Theme.cornerBracketArmLength
+ thickness: Theme.cornerBracketThickness
+ z: 10
+ opacity: root.isOpen ? 1.0 : 0.0
+ visible: opacity > 0.0
+
+ Behavior on opacity {
+ NumberAnimation {
+ duration: Settings.reducedMotion ? 0 : Theme.durationFast
}
}
}
- // Header Divider & Tabs
+ // Main Content Column
ColumnLayout {
- Layout.fillWidth: true
- spacing: 0
+ id: mainLayout
+ anchors.fill: parent
+ anchors.margins: Theme.paddingXl
+ spacing: Theme.spacingMedium
+ opacity: root.isOpen ? 1.0 : 0.0
+ visible: opacity > 0.0
+ Behavior on opacity {
+ NumberAnimation {
+ duration: Settings.reducedMotion ? 0 : Theme.durationFast
+ }
+ }
+
+ // Header Row
RowLayout {
Layout.fillWidth: true
spacing: Theme.spacingMedium
Rectangle {
- id: tab0
+ Layout.preferredHeight: 8
+ Layout.preferredWidth: 8
+ color: Theme.accent
+ radius: 4
+ }
+
+ Text {
+ Layout.fillWidth: true
+ color: Theme.accent
+ font.family: Theme.fontFamilyMonospace
+ font.pixelSize: Theme.fontSizeSegment
+ font.weight: Theme.fontWeightBold
+ text: "// EVENT LOG"
+ }
+
+ // DND Toggle Button
+ Rectangle {
+ id: dndButton
Layout.preferredHeight: 24
- Layout.preferredWidth: tab0Text.implicitWidth + Theme.paddingMedium * 2
- color: root.currentTab === 0 ? Theme.surfaceSelected : (tab0MouseArea.containsMouse ? Theme.surfaceHover : "transparent")
- border.color: root.currentTab === 0 ? Theme.accent : Theme.borderMuted
+ Layout.preferredWidth: dndLabel.implicitWidth + Theme.paddingMedium * 2
+ border.color: NotificationService.doNotDisturb ? Theme.warningRed : (dndMouseArea.containsMouse ? Theme.accent : Theme.borderMuted)
border.width: Theme.borderWidth
+ color: NotificationService.doNotDisturb ? Theme.surfaceSelected : (dndMouseArea.containsMouse ? Theme.surfaceHover : "transparent")
radius: Theme.radiusSmall
Text {
- id: tab0Text
+ id: dndLabel
anchors.centerIn: parent
- color: root.currentTab === 0 ? Theme.accent : Theme.textSecondary
+ color: NotificationService.doNotDisturb ? Theme.warningRed : (dndMouseArea.containsMouse ? Theme.accent : Theme.textSecondary)
font.family: Theme.fontFamilyMonospace
font.pixelSize: Theme.fontSizeCaption
font.weight: Theme.fontWeightBold
- text: "NOTIFICATIONS"
+ text: NotificationService.doNotDisturb ? "[DND]" : "[DND OFF]"
}
MouseArea {
- id: tab0MouseArea
+ id: dndMouseArea
anchors.fill: parent
- hoverEnabled: true
cursorShape: Qt.PointingHandCursor
- onClicked: root.currentTab = 0
+ hoverEnabled: true
+ onClicked: NotificationService.toggleDnd()
}
}
+ // Close Button
Rectangle {
- id: tab1
+ id: closeButton
Layout.preferredHeight: 24
- Layout.preferredWidth: tab1Text.implicitWidth + Theme.paddingMedium * 2
- color: root.currentTab === 1 ? Theme.surfaceSelected : (tab1MouseArea.containsMouse ? Theme.surfaceHover : "transparent")
- border.color: root.currentTab === 1 ? Theme.accent : Theme.borderMuted
+ Layout.preferredWidth: 24
+ border.color: closeMouseArea.containsMouse ? Theme.accent : Theme.borderMuted
border.width: Theme.borderWidth
+ color: closeMouseArea.containsMouse ? Theme.surfaceHover : "transparent"
radius: Theme.radiusSmall
Text {
- id: tab1Text
anchors.centerIn: parent
- color: root.currentTab === 1 ? Theme.accent : Theme.textSecondary
+ color: closeMouseArea.containsMouse ? Theme.accent : Theme.textSecondary
font.family: Theme.fontFamilyMonospace
font.pixelSize: Theme.fontSizeCaption
font.weight: Theme.fontWeightBold
- text: "SYSTEM LOGS"
+ text: "x"
}
MouseArea {
- id: tab1MouseArea
+ id: closeMouseArea
anchors.fill: parent
- hoverEnabled: true
cursorShape: Qt.PointingHandCursor
- onClicked: root.currentTab = 1
+ hoverEnabled: true
+ onClicked: OverlayController.close()
}
}
}
- Item {
- Layout.preferredHeight: Theme.spacingMedium
- }
-
- Rectangle {
+ // Header Divider & Tabs
+ ColumnLayout {
Layout.fillWidth: true
- Layout.preferredHeight: Theme.borderWidth
- color: Theme.borderMuted
- }
- }
+ spacing: 0
- // Body Content Area (ListView or Empty State)
- Item {
- id: contentContainer
- Layout.fillWidth: true
- Layout.fillHeight: true
- visible: root.currentTab === 0
-
- // Notification History ListView
- ListView {
- id: historyListView
- anchors.fill: parent
- clip: true
- boundsBehavior: Flickable.StopAtBounds
- spacing: Theme.spacingSmall
- model: NotificationService.history
- visible: NotificationService.history.count > 0
-
- delegate: Rectangle {
- id: cardItem
- required property int index
- required property var notifId
- required property string appName
- required property string summary
- required property string body
- required property int urgency
- required property string timestamp
-
- width: historyListView.width
- implicitHeight: cardLayout.implicitHeight + Theme.paddingMedium * 2
- color: Theme.gray800
- border.color: Theme.gray700
- border.width: Theme.borderWidth
- radius: Theme.radiusSmall
+ RowLayout {
+ Layout.fillWidth: true
+ spacing: Theme.spacingMedium
- readonly property color urgencyColor: {
- if (cardItem.urgency === 2) {
- return Theme.warningRed;
+ Rectangle {
+ id: tab0
+ Layout.preferredHeight: 24
+ Layout.preferredWidth: tab0Text.implicitWidth + Theme.paddingMedium * 2
+ color: root.currentTab === 0 ? Theme.surfaceSelected : (tab0MouseArea.containsMouse ? Theme.surfaceHover : "transparent")
+ border.color: root.currentTab === 0 ? Theme.accent : Theme.borderMuted
+ border.width: Theme.borderWidth
+ radius: Theme.radiusSmall
+
+ Text {
+ id: tab0Text
+ anchors.centerIn: parent
+ color: root.currentTab === 0 ? Theme.accent : Theme.textSecondary
+ font.family: Theme.fontFamilyMonospace
+ font.pixelSize: Theme.fontSizeCaption
+ font.weight: Theme.fontWeightBold
+ text: "NOTIFICATIONS"
}
- if (cardItem.urgency === 0) {
- return Theme.textMuted;
+
+ MouseArea {
+ id: tab0MouseArea
+ anchors.fill: parent
+ hoverEnabled: true
+ cursorShape: Qt.PointingHandCursor
+ onClicked: root.currentTab = 0
}
- return Theme.acidGreen;
}
- // Urgency accent stripe on left edge
Rectangle {
- id: urgencyStripe
- anchors.bottom: parent.bottom
- anchors.left: parent.left
- anchors.top: parent.top
- color: cardItem.urgencyColor
+ id: tab1
+ Layout.preferredHeight: 24
+ Layout.preferredWidth: tab1Text.implicitWidth + Theme.paddingMedium * 2
+ color: root.currentTab === 1 ? Theme.surfaceSelected : (tab1MouseArea.containsMouse ? Theme.surfaceHover : "transparent")
+ border.color: root.currentTab === 1 ? Theme.accent : Theme.borderMuted
+ border.width: Theme.borderWidth
radius: Theme.radiusSmall
- width: 3
+
+ Text {
+ id: tab1Text
+ anchors.centerIn: parent
+ color: root.currentTab === 1 ? Theme.accent : Theme.textSecondary
+ font.family: Theme.fontFamilyMonospace
+ font.pixelSize: Theme.fontSizeCaption
+ font.weight: Theme.fontWeightBold
+ text: "SYSTEM LOGS"
+ }
+
+ MouseArea {
+ id: tab1MouseArea
+ anchors.fill: parent
+ hoverEnabled: true
+ cursorShape: Qt.PointingHandCursor
+ onClicked: root.currentTab = 1
+ }
}
+ }
- ColumnLayout {
- id: cardLayout
- anchors.left: urgencyStripe.right
- anchors.right: parent.right
- anchors.top: parent.top
- anchors.margins: Theme.paddingMedium
- spacing: Theme.spacingSmall
-
- // Header line: Urgency Dot, App Name (left), Timestamp (right)
- RowLayout {
- Layout.fillWidth: true
+ Item {
+ Layout.preferredHeight: Theme.spacingMedium
+ }
+
+ Rectangle {
+ Layout.fillWidth: true
+ Layout.preferredHeight: Theme.borderWidth
+ color: Theme.borderMuted
+ }
+ }
+
+ // Body Content Area (ListView or Empty State)
+ Item {
+ id: contentContainer
+ Layout.fillWidth: true
+ Layout.fillHeight: true
+ visible: root.currentTab === 0
+
+ // Notification History ListView
+ ListView {
+ id: historyListView
+ anchors.fill: parent
+ clip: true
+ boundsBehavior: Flickable.StopAtBounds
+ spacing: Theme.spacingSmall
+ model: NotificationService.history
+ visible: NotificationService.history.count > 0
+
+ delegate: Rectangle {
+ id: cardItem
+ required property int index
+ required property var notifId
+ required property string appName
+ required property string summary
+ required property string body
+ required property int urgency
+ required property string timestamp
+
+ width: historyListView.width
+ implicitHeight: cardLayout.implicitHeight + Theme.paddingMedium * 2
+ color: Theme.gray800
+ border.color: Theme.gray700
+ border.width: Theme.borderWidth
+ radius: Theme.radiusSmall
+
+ readonly property color urgencyColor: {
+ if (cardItem.urgency === 2) {
+ return Theme.warningRed;
+ }
+ if (cardItem.urgency === 0) {
+ return Theme.textMuted;
+ }
+ return Theme.acidGreen;
+ }
+
+ // Urgency accent stripe on left edge
+ Rectangle {
+ id: urgencyStripe
+ anchors.bottom: parent.bottom
+ anchors.left: parent.left
+ anchors.top: parent.top
+ color: cardItem.urgencyColor
+ radius: Theme.radiusSmall
+ width: 3
+ }
+
+ ColumnLayout {
+ id: cardLayout
+ anchors.left: urgencyStripe.right
+ anchors.right: parent.right
+ anchors.top: parent.top
+ anchors.margins: Theme.paddingMedium
spacing: Theme.spacingSmall
- Rectangle {
- Layout.preferredHeight: 6
- Layout.preferredWidth: 6
- color: cardItem.urgencyColor
- radius: 3
+ // Header line: Urgency Dot, App Name (left), Timestamp (right)
+ RowLayout {
+ Layout.fillWidth: true
+ spacing: Theme.spacingSmall
+
+ Rectangle {
+ Layout.preferredHeight: 6
+ Layout.preferredWidth: 6
+ color: cardItem.urgencyColor
+ radius: 3
+ }
+
+ Text {
+ Layout.fillWidth: true
+ color: Theme.textSecondary
+ elide: Text.ElideRight
+ font.family: Theme.fontFamilyMonospace
+ font.pixelSize: Theme.fontSizeCaption
+ font.weight: Theme.fontWeightMedium
+ text: cardItem.appName.toUpperCase()
+ }
+
+ Text {
+ color: Theme.textMuted
+ font.family: Theme.fontFamilyMonospace
+ font.pixelSize: Theme.fontSizeCaption
+ text: cardItem.timestamp
+ }
}
+ // Summary text
Text {
Layout.fillWidth: true
- color: Theme.textSecondary
+ color: Theme.textPrimary
elide: Text.ElideRight
font.family: Theme.fontFamilyMonospace
- font.pixelSize: Theme.fontSizeCaption
- font.weight: Theme.fontWeightMedium
- text: cardItem.appName.toUpperCase()
+ font.pixelSize: Theme.fontSizeSmall
+ font.weight: Theme.fontWeightBold
+ text: cardItem.summary
}
+ // Body text (word wrap, max 3 lines, elide)
Text {
- color: Theme.textMuted
+ Layout.fillWidth: true
+ color: Theme.textSecondary
+ elide: Text.ElideRight
font.family: Theme.fontFamilyMonospace
font.pixelSize: Theme.fontSizeCaption
- text: cardItem.timestamp
+ maximumLineCount: 3
+ text: cardItem.body
+ visible: cardItem.body !== ""
+ wrapMode: Text.WordWrap
}
- }
-
- // Summary text
- Text {
- Layout.fillWidth: true
- color: Theme.textPrimary
- elide: Text.ElideRight
- font.family: Theme.fontFamilyMonospace
- font.pixelSize: Theme.fontSizeSmall
- font.weight: Theme.fontWeightBold
- text: cardItem.summary
- }
-
- // Body text (word wrap, max 3 lines, elide)
- Text {
- Layout.fillWidth: true
- color: Theme.textSecondary
- elide: Text.ElideRight
- font.family: Theme.fontFamilyMonospace
- font.pixelSize: Theme.fontSizeCaption
- maximumLineCount: 3
- text: cardItem.body
- visible: cardItem.body !== ""
- wrapMode: Text.WordWrap
- }
-
- // Dismiss action button
- RowLayout {
- Layout.fillWidth: true
- Item {
+ // Dismiss action button
+ RowLayout {
Layout.fillWidth: true
- }
-
- Rectangle {
- id: dismissBtn
- Layout.preferredHeight: 20
- Layout.preferredWidth: dismissText.implicitWidth + Theme.paddingSmall * 2
- border.color: dismissMouseArea.containsMouse ? Theme.accent : Theme.borderMuted
- border.width: Theme.borderWidth
- color: dismissMouseArea.containsMouse ? Theme.surfaceHover : "transparent"
- radius: Theme.radiusSmall
- Text {
- id: dismissText
- anchors.centerIn: parent
- color: dismissMouseArea.containsMouse ? Theme.accent : Theme.textSecondary
- font.family: Theme.fontFamilyMonospace
- font.pixelSize: Theme.fontSizeCaption
- font.weight: Theme.fontWeightMedium
- text: "[DISMISS]"
+ Item {
+ Layout.fillWidth: true
}
- MouseArea {
- id: dismissMouseArea
- anchors.fill: parent
- cursorShape: Qt.PointingHandCursor
- hoverEnabled: true
- onClicked: NotificationService.dismissHistoryItem(cardItem.index)
+ Rectangle {
+ id: dismissBtn
+ Layout.preferredHeight: 20
+ Layout.preferredWidth: dismissText.implicitWidth + Theme.paddingSmall * 2
+ border.color: dismissMouseArea.containsMouse ? Theme.accent : Theme.borderMuted
+ border.width: Theme.borderWidth
+ color: dismissMouseArea.containsMouse ? Theme.surfaceHover : "transparent"
+ radius: Theme.radiusSmall
+
+ Text {
+ id: dismissText
+ anchors.centerIn: parent
+ color: dismissMouseArea.containsMouse ? Theme.accent : Theme.textSecondary
+ font.family: Theme.fontFamilyMonospace
+ font.pixelSize: Theme.fontSizeCaption
+ font.weight: Theme.fontWeightMedium
+ text: "[DISMISS]"
+ }
+
+ MouseArea {
+ id: dismissMouseArea
+ anchors.fill: parent
+ cursorShape: Qt.PointingHandCursor
+ hoverEnabled: true
+ onClicked: NotificationService.dismissHistoryItem(cardItem.index)
+ }
}
}
}
}
}
- }
-
- // Scrollbar track & thumb
- Rectangle {
- id: scrollTrack
- anchors.bottom: parent.bottom
- anchors.right: parent.right
- anchors.top: parent.top
- color: Theme.gray800
- radius: 2
- visible: historyListView.visible && (historyListView.height < historyListView.contentHeight)
- width: 4
+ // Scrollbar track & thumb
Rectangle {
- id: scrollThumb
- color: Theme.acidGreen
- height: Math.max(16, historyListView.visibleArea.heightRatio * historyListView.height)
+ id: scrollTrack
+ anchors.bottom: parent.bottom
+ anchors.right: parent.right
+ anchors.top: parent.top
+ color: Theme.gray800
radius: 2
+ visible: historyListView.visible && (historyListView.height < historyListView.contentHeight)
width: 4
- y: historyListView.visibleArea.yPosition * historyListView.height
+
+ Rectangle {
+ id: scrollThumb
+ color: Theme.acidGreen
+ height: Math.max(16, historyListView.visibleArea.heightRatio * historyListView.height)
+ radius: 2
+ width: 4
+ y: historyListView.visibleArea.yPosition * historyListView.height
+ }
+ }
+
+ // Empty state container
+ Item {
+ id: emptyState
+ anchors.fill: parent
+ visible: NotificationService.history.count === 0
+
+ ColumnLayout {
+ anchors.centerIn: parent
+ spacing: Theme.spacingMedium
+
+ Rectangle {
+ Layout.alignment: Qt.AlignHCenter
+ Layout.preferredHeight: 8
+ Layout.preferredWidth: 8
+ color: Theme.textMuted
+ radius: 4
+ }
+
+ Text {
+ Layout.alignment: Qt.AlignHCenter
+ color: Theme.textMuted
+ font.family: Theme.fontFamilyMonospace
+ font.pixelSize: Theme.fontSizeSmall
+ font.weight: Theme.fontWeightMedium
+ text: "NO NOTIFICATIONS // STANDBY"
+ }
+ }
}
}
- // Empty state container
+ // System Logs Content Area
Item {
- id: emptyState
- anchors.fill: parent
- visible: NotificationService.history.count === 0
+ id: systemLogsContainer
+ Layout.fillWidth: true
+ Layout.fillHeight: true
+ visible: root.currentTab === 1
ColumnLayout {
anchors.centerIn: parent
@@ -439,99 +519,69 @@ FocusScope {
font.family: Theme.fontFamilyMonospace
font.pixelSize: Theme.fontSizeSmall
font.weight: Theme.fontWeightMedium
- text: "NO NOTIFICATIONS // STANDBY"
+ text: "SYSTEM LOGS // UNAVAILABLE"
}
}
}
- }
-
- // System Logs Content Area
- Item {
- id: systemLogsContainer
- Layout.fillWidth: true
- Layout.fillHeight: true
- visible: root.currentTab === 1
-
- ColumnLayout {
- anchors.centerIn: parent
- spacing: Theme.spacingMedium
-
- Rectangle {
- Layout.alignment: Qt.AlignHCenter
- Layout.preferredHeight: 8
- Layout.preferredWidth: 8
- color: Theme.textMuted
- radius: 4
- }
-
- Text {
- Layout.alignment: Qt.AlignHCenter
- color: Theme.textMuted
- font.family: Theme.fontFamilyMonospace
- font.pixelSize: Theme.fontSizeSmall
- font.weight: Theme.fontWeightMedium
- text: "SYSTEM LOGS // UNAVAILABLE"
- }
- }
- }
-
- // Footer Divider
- Rectangle {
- Layout.fillWidth: true
- Layout.preferredHeight: Theme.borderWidth
- color: Theme.borderMuted
- visible: root.currentTab === 0
- }
-
- // Footer Action Row
- RowLayout {
- Layout.fillWidth: true
- spacing: Theme.spacingMedium
- visible: root.currentTab === 0
- Text {
+ // Footer Divider
+ Rectangle {
Layout.fillWidth: true
- color: Theme.textSecondary
- font.family: Theme.fontFamilyMonospace
- font.pixelSize: Theme.fontSizeCaption
- text: NotificationService.history.count > 0
- ? NotificationService.history.count + " LOGGED"
- : "0 LOGGED"
+ Layout.preferredHeight: Theme.borderWidth
+ color: Theme.borderMuted
+ visible: root.currentTab === 0
}
- Rectangle {
- id: clearAllBtn
- Layout.preferredHeight: 26
- Layout.preferredWidth: clearAllText.implicitWidth + Theme.paddingMedium * 2
- border.color: NotificationService.history.count === 0
- ? Theme.borderMuted
- : (clearMouseArea.containsMouse ? Theme.destructive : Theme.borderMuted)
- border.width: Theme.borderWidth
- color: NotificationService.history.count === 0
- ? "transparent"
- : (clearMouseArea.containsMouse ? Theme.surfaceHover : "transparent")
- opacity: NotificationService.history.count === 0 ? 0.4 : 1.0
- radius: Theme.radiusSmall
+ // Footer Action Row
+ RowLayout {
+ Layout.fillWidth: true
+ spacing: Theme.spacingMedium
+ visible: root.currentTab === 0
Text {
- id: clearAllText
- anchors.centerIn: parent
- color: NotificationService.history.count === 0
- ? Theme.textMuted
- : (clearMouseArea.containsMouse ? Theme.destructive : Theme.textPrimary)
+ Layout.fillWidth: true
+ color: Theme.textSecondary
font.family: Theme.fontFamilyMonospace
font.pixelSize: Theme.fontSizeCaption
- font.weight: Theme.fontWeightBold
- text: "[CLEAR ALL]"
+ text: NotificationService.history.count > 0
+ ? NotificationService.history.count + " LOGGED"
+ : "0 LOGGED"
}
- MouseArea {
- id: clearMouseArea
- anchors.fill: parent
- cursorShape: NotificationService.history.count > 0 ? Qt.PointingHandCursor : Qt.ArrowCursor
- enabled: NotificationService.history.count > 0
- hoverEnabled: NotificationService.history.count > 0
- onClicked: NotificationService.clearAll()
+ Rectangle {
+ id: clearAllBtn
+ Layout.preferredHeight: 26
+ Layout.preferredWidth: clearAllText.implicitWidth + Theme.paddingMedium * 2
+ border.color: NotificationService.history.count === 0
+ ? Theme.borderMuted
+ : (clearMouseArea.containsMouse ? Theme.destructive : Theme.borderMuted)
+ border.width: Theme.borderWidth
+ color: NotificationService.history.count === 0
+ ? "transparent"
+ : (clearMouseArea.containsMouse ? Theme.surfaceHover : "transparent")
+ opacity: NotificationService.history.count === 0 ? 0.4 : 1.0
+ radius: Theme.radiusSmall
+
+ Text {
+ id: clearAllText
+ anchors.centerIn: parent
+ color: NotificationService.history.count === 0
+ ? Theme.textMuted
+ : (clearMouseArea.containsMouse ? Theme.destructive : Theme.textPrimary)
+ font.family: Theme.fontFamilyMonospace
+ font.pixelSize: Theme.fontSizeCaption
+ font.weight: Theme.fontWeightBold
+ text: "[CLEAR ALL]"
+ }
+
+ MouseArea {
+ id: clearMouseArea
+ anchors.fill: parent
+ cursorShape: NotificationService.history.count > 0 ? Qt.PointingHandCursor : Qt.ArrowCursor
+ enabled: NotificationService.history.count > 0
+ hoverEnabled: NotificationService.history.count > 0
+ onClicked: NotificationService.clearAll()
+ }
}
}
}
diff --git a/shell/desktop/surfaces/components/DynamicIsland.qml b/shell/desktop/surfaces/components/DynamicIsland.qml
index 90688078..4239b235 100644
--- a/shell/desktop/surfaces/components/DynamicIsland.qml
+++ b/shell/desktop/surfaces/components/DynamicIsland.qml
@@ -5,7 +5,6 @@ import QtQuick.Layouts
import Quickshell
import "../../core"
import "../../services"
-import "../widgets"
Rectangle {
id: root
@@ -16,10 +15,8 @@ Rectangle {
readonly property real compactWidth: 120
readonly property real expandedWidth: 300
- readonly property real eventLogWidth: 360
- readonly property real eventLogHeight: 500
- // Three-state property: "compact", "notification", "eventLog"
+ // Two primary states: "compact", "notification"
property string state: "compact"
// Writable isExpanded for backward compatibility with tests.
@@ -32,7 +29,7 @@ Rectangle {
property int latestUrgency: 1
readonly property bool isEventLogOpen: OverlayController.activeSurface === OverlayController.Surface.EventLog
- // Sync isExpanded writes → state
+ // Sync isExpanded writes -> state
onIsExpandedChanged: {
if (root.isExpanded && root.state === "compact") {
root.state = "notification";
@@ -41,7 +38,7 @@ Rectangle {
}
}
- // Sync state → isExpanded (for notification state changes from showNotification)
+ // Sync state -> isExpanded (for notification state changes from showNotification)
onStateChanged: {
if (root.state === "notification" && !root.isExpanded) {
root.isExpanded = true;
@@ -53,13 +50,11 @@ Rectangle {
// Centralized layout function per dynamic-radial-geometry skill:
// Synchronously compute width/height based on state to avoid desync.
function _computeLayout(): var {
- if (root.state === "eventLog") {
- return { w: root.eventLogWidth, h: root.eventLogHeight };
- } else if (root.state === "notification") {
- return { w: root.expandedWidth, h: Theme.barHeight - 6 };
- } else {
- return { w: root.compactWidth, h: Theme.barHeight - 6 };
- }
+ const isExp = root.isExpanded || root.state === "notification";
+ return {
+ w: isExp ? root.expandedWidth : root.compactWidth,
+ h: Theme.barHeight - 6
+ };
}
property var _layout: _computeLayout()
@@ -70,23 +65,26 @@ Rectangle {
implicitHeight: height
color: Theme.background
- radius: Theme.radiusMedium
- border.color: mouseArea.containsMouse && root.state !== "eventLog" ? Theme.accent : Theme.borderMuted
+ radius: Theme.radiusPill
+ border.color: mouseArea.containsMouse ? Theme.accent : Theme.borderMuted
border.width: Theme.borderWidth
clip: true
- // =========================================================================
- // Expansion / Collapse Animation
- // =========================================================================
+ // Approach A: When EventLog overlay is active, set opacity to 0
+ opacity: root.isEventLogOpen ? 0.0 : 1.0
- Behavior on width {
+ Behavior on opacity {
NumberAnimation {
duration: Settings.reducedMotion ? 0 : Theme.durationSlow
easing.type: Easing.InOutQuad
}
}
- Behavior on height {
+ // =========================================================================
+ // Expansion / Collapse Animation
+ // =========================================================================
+
+ Behavior on width {
NumberAnimation {
duration: Settings.reducedMotion ? 0 : Theme.durationSlow
easing.type: Easing.InOutQuad
@@ -119,7 +117,6 @@ Rectangle {
Connections {
target: NotificationService
- // Handles explicit signal if present
function onNotificationReceived(notification): void {
if (!NotificationService.doNotDisturb && notification) {
root.showNotification(notification.appName, notification.summary, notification.urgency);
@@ -141,39 +138,6 @@ Rectangle {
}
}
- // =========================================================================
- // OverlayController Connection — react to external close
- // =========================================================================
-
- Connections {
- target: OverlayController
-
- function onOverlayClosed(previousSurface: int): void {
- if (previousSurface === OverlayController.Surface.EventLog && root.state === "eventLog") {
- root.state = "compact";
- }
- }
-
- function onOverlayOpened(surface: int): void {
- if (surface === OverlayController.Surface.EventLog && root.state === "eventLog") {
- root.forceActiveFocus();
- }
- }
- }
-
- // =========================================================================
- // Keyboard Handling (Escape to close eventLog)
- // =========================================================================
-
- Keys.onEscapePressed: function (event) {
- if (root.state === "eventLog") {
- OverlayController.close();
- if (event) {
- event.accepted = true;
- }
- }
- }
-
// =========================================================================
// Compact View
// =========================================================================
@@ -181,7 +145,7 @@ Rectangle {
RowLayout {
id: compactContent
anchors.centerIn: parent
- opacity: root.state === "compact" ? 1.0 : 0.0
+ opacity: (!root.isExpanded && root.state === "compact") ? 1.0 : 0.0
visible: opacity > 0.0
spacing: Theme.spacingSmall
@@ -254,7 +218,7 @@ Rectangle {
anchors.fill: parent
anchors.leftMargin: Theme.paddingLarge
anchors.rightMargin: Theme.paddingLarge
- opacity: root.state === "notification" ? 1.0 : 0.0
+ opacity: (root.isExpanded || root.state === "notification") ? 1.0 : 0.0
visible: opacity > 0.0
spacing: Theme.spacingSmall
@@ -305,518 +269,6 @@ Rectangle {
}
}
- // =========================================================================
- // Event Log View (Embedded — morphs from DynamicIsland)
- // =========================================================================
-
- property int eventLogCurrentTab: 0
-
- // Corner Brackets decoration (cyber aesthetic) — visible only in eventLog state
- CornerBrackets {
- id: cornerBrackets
- bracketColor: Theme.acidGreen
- margin: Theme.cornerBracketMargin
- armLength: Theme.cornerBracketArmLength
- thickness: Theme.cornerBracketThickness
- z: 10
- opacity: root.state === "eventLog" ? 1.0 : 0.0
- visible: opacity > 0.0
-
- Behavior on opacity {
- NumberAnimation {
- duration: Settings.reducedMotion ? 0 : Theme.durationFast
- }
- }
- }
-
- // Event Log Content
- Item {
- id: eventLogContent
- anchors.fill: parent
- opacity: root.state === "eventLog" ? 1.0 : 0.0
- visible: opacity > 0.0
-
- Behavior on opacity {
- NumberAnimation {
- duration: Settings.reducedMotion ? 0 : Theme.durationFast
- }
- }
-
- // Inside Click Consumer — prevent clicks from falling through
- MouseArea {
- id: eventLogInsideClickConsumer
- anchors.fill: parent
- hoverEnabled: true
- preventStealing: true
- onClicked: function (mouse) {
- mouse.accepted = true;
- }
- }
-
- // Main Content Column
- ColumnLayout {
- id: eventLogMainLayout
- anchors.fill: parent
- anchors.margins: Theme.paddingXl
- spacing: Theme.spacingMedium
-
- // Header Row
- RowLayout {
- Layout.fillWidth: true
- spacing: Theme.spacingMedium
-
- Rectangle {
- Layout.preferredHeight: 8
- Layout.preferredWidth: 8
- color: Theme.accent
- radius: 4
- }
-
- Text {
- Layout.fillWidth: true
- color: Theme.accent
- font.family: Theme.fontFamilyMonospace
- font.pixelSize: Theme.fontSizeSegment
- font.weight: Theme.fontWeightBold
- text: "// EVENT LOG"
- }
-
- // DND Toggle Button
- Rectangle {
- id: eventLogDndButton
- Layout.preferredHeight: 24
- Layout.preferredWidth: eventLogDndLabel.implicitWidth + Theme.paddingMedium * 2
- border.color: NotificationService.doNotDisturb ? Theme.warningRed : (eventLogDndMouseArea.containsMouse ? Theme.accent : Theme.borderMuted)
- border.width: Theme.borderWidth
- color: NotificationService.doNotDisturb ? Theme.surfaceSelected : (eventLogDndMouseArea.containsMouse ? Theme.surfaceHover : "transparent")
- radius: Theme.radiusSmall
-
- Text {
- id: eventLogDndLabel
- anchors.centerIn: parent
- color: NotificationService.doNotDisturb ? Theme.warningRed : (eventLogDndMouseArea.containsMouse ? Theme.accent : Theme.textSecondary)
- font.family: Theme.fontFamilyMonospace
- font.pixelSize: Theme.fontSizeCaption
- font.weight: Theme.fontWeightBold
- text: NotificationService.doNotDisturb ? "[DND]" : "[DND OFF]"
- }
-
- MouseArea {
- id: eventLogDndMouseArea
- anchors.fill: parent
- cursorShape: Qt.PointingHandCursor
- hoverEnabled: true
- onClicked: NotificationService.toggleDnd()
- }
- }
-
- // Close Button
- Rectangle {
- id: eventLogCloseButton
- Layout.preferredHeight: 24
- Layout.preferredWidth: 24
- border.color: eventLogCloseMouseArea.containsMouse ? Theme.accent : Theme.borderMuted
- border.width: Theme.borderWidth
- color: eventLogCloseMouseArea.containsMouse ? Theme.surfaceHover : "transparent"
- radius: Theme.radiusSmall
-
- Text {
- anchors.centerIn: parent
- color: eventLogCloseMouseArea.containsMouse ? Theme.accent : Theme.textSecondary
- font.family: Theme.fontFamilyMonospace
- font.pixelSize: Theme.fontSizeCaption
- font.weight: Theme.fontWeightBold
- text: "x"
- }
-
- MouseArea {
- id: eventLogCloseMouseArea
- anchors.fill: parent
- cursorShape: Qt.PointingHandCursor
- hoverEnabled: true
- onClicked: OverlayController.close()
- }
- }
- }
-
- // Header Divider & Tabs
- ColumnLayout {
- Layout.fillWidth: true
- spacing: 0
-
- RowLayout {
- Layout.fillWidth: true
- spacing: Theme.spacingMedium
-
- Rectangle {
- id: eventLogTab0
- Layout.preferredHeight: 24
- Layout.preferredWidth: eventLogTab0Text.implicitWidth + Theme.paddingMedium * 2
- color: root.eventLogCurrentTab === 0 ? Theme.surfaceSelected : (eventLogTab0MouseArea.containsMouse ? Theme.surfaceHover : "transparent")
- border.color: root.eventLogCurrentTab === 0 ? Theme.accent : Theme.borderMuted
- border.width: Theme.borderWidth
- radius: Theme.radiusSmall
-
- Text {
- id: eventLogTab0Text
- anchors.centerIn: parent
- color: root.eventLogCurrentTab === 0 ? Theme.accent : Theme.textSecondary
- font.family: Theme.fontFamilyMonospace
- font.pixelSize: Theme.fontSizeCaption
- font.weight: Theme.fontWeightBold
- text: "NOTIFICATIONS"
- }
-
- MouseArea {
- id: eventLogTab0MouseArea
- anchors.fill: parent
- hoverEnabled: true
- cursorShape: Qt.PointingHandCursor
- onClicked: root.eventLogCurrentTab = 0
- }
- }
-
- Rectangle {
- id: eventLogTab1
- Layout.preferredHeight: 24
- Layout.preferredWidth: eventLogTab1Text.implicitWidth + Theme.paddingMedium * 2
- color: root.eventLogCurrentTab === 1 ? Theme.surfaceSelected : (eventLogTab1MouseArea.containsMouse ? Theme.surfaceHover : "transparent")
- border.color: root.eventLogCurrentTab === 1 ? Theme.accent : Theme.borderMuted
- border.width: Theme.borderWidth
- radius: Theme.radiusSmall
-
- Text {
- id: eventLogTab1Text
- anchors.centerIn: parent
- color: root.eventLogCurrentTab === 1 ? Theme.accent : Theme.textSecondary
- font.family: Theme.fontFamilyMonospace
- font.pixelSize: Theme.fontSizeCaption
- font.weight: Theme.fontWeightBold
- text: "SYSTEM LOGS"
- }
-
- MouseArea {
- id: eventLogTab1MouseArea
- anchors.fill: parent
- hoverEnabled: true
- cursorShape: Qt.PointingHandCursor
- onClicked: root.eventLogCurrentTab = 1
- }
- }
- }
-
- Item {
- Layout.preferredHeight: Theme.spacingMedium
- }
-
- Rectangle {
- Layout.fillWidth: true
- Layout.preferredHeight: Theme.borderWidth
- color: Theme.borderMuted
- }
- }
-
- // Body Content Area (ListView or Empty State)
- Item {
- id: eventLogContentContainer
- Layout.fillWidth: true
- Layout.fillHeight: true
- visible: root.eventLogCurrentTab === 0
-
- // Notification History ListView
- ListView {
- id: eventLogHistoryListView
- anchors.fill: parent
- clip: true
- boundsBehavior: Flickable.StopAtBounds
- spacing: Theme.spacingSmall
- model: NotificationService.history
- visible: NotificationService.history.count > 0
-
- delegate: Rectangle {
- id: eventLogCardItem
- required property int index
- required property var notifId
- required property string appName
- required property string summary
- required property string body
- required property int urgency
- required property string timestamp
-
- width: eventLogHistoryListView.width
- implicitHeight: eventLogCardLayout.implicitHeight + Theme.paddingMedium * 2
- color: Theme.gray800
- border.color: Theme.gray700
- border.width: Theme.borderWidth
- radius: Theme.radiusSmall
-
- readonly property color urgencyColor: {
- if (eventLogCardItem.urgency === 2) {
- return Theme.warningRed;
- }
- if (eventLogCardItem.urgency === 0) {
- return Theme.textMuted;
- }
- return Theme.acidGreen;
- }
-
- // Urgency accent stripe on left edge
- Rectangle {
- id: eventLogUrgencyStripe
- anchors.bottom: parent.bottom
- anchors.left: parent.left
- anchors.top: parent.top
- color: eventLogCardItem.urgencyColor
- radius: Theme.radiusSmall
- width: 3
- }
-
- ColumnLayout {
- id: eventLogCardLayout
- anchors.left: eventLogUrgencyStripe.right
- anchors.right: parent.right
- anchors.top: parent.top
- anchors.margins: Theme.paddingMedium
- spacing: Theme.spacingSmall
-
- // Header line: Urgency Dot, App Name (left), Timestamp (right)
- RowLayout {
- Layout.fillWidth: true
- spacing: Theme.spacingSmall
-
- Rectangle {
- Layout.preferredHeight: 6
- Layout.preferredWidth: 6
- color: eventLogCardItem.urgencyColor
- radius: 3
- }
-
- Text {
- Layout.fillWidth: true
- color: Theme.textSecondary
- elide: Text.ElideRight
- font.family: Theme.fontFamilyMonospace
- font.pixelSize: Theme.fontSizeCaption
- font.weight: Theme.fontWeightMedium
- text: eventLogCardItem.appName.toUpperCase()
- }
-
- Text {
- color: Theme.textMuted
- font.family: Theme.fontFamilyMonospace
- font.pixelSize: Theme.fontSizeCaption
- text: eventLogCardItem.timestamp
- }
- }
-
- // Summary text
- Text {
- Layout.fillWidth: true
- color: Theme.textPrimary
- elide: Text.ElideRight
- font.family: Theme.fontFamilyMonospace
- font.pixelSize: Theme.fontSizeSmall
- font.weight: Theme.fontWeightBold
- text: eventLogCardItem.summary
- }
-
- // Body text (word wrap, max 3 lines, elide)
- Text {
- Layout.fillWidth: true
- color: Theme.textSecondary
- elide: Text.ElideRight
- font.family: Theme.fontFamilyMonospace
- font.pixelSize: Theme.fontSizeCaption
- maximumLineCount: 3
- text: eventLogCardItem.body
- visible: eventLogCardItem.body !== ""
- wrapMode: Text.WordWrap
- }
-
- // Dismiss action button
- RowLayout {
- Layout.fillWidth: true
-
- Item {
- Layout.fillWidth: true
- }
-
- Rectangle {
- id: eventLogDismissBtn
- Layout.preferredHeight: 20
- Layout.preferredWidth: eventLogDismissText.implicitWidth + Theme.paddingSmall * 2
- border.color: eventLogDismissMouseArea.containsMouse ? Theme.accent : Theme.borderMuted
- border.width: Theme.borderWidth
- color: eventLogDismissMouseArea.containsMouse ? Theme.surfaceHover : "transparent"
- radius: Theme.radiusSmall
-
- Text {
- id: eventLogDismissText
- anchors.centerIn: parent
- color: eventLogDismissMouseArea.containsMouse ? Theme.accent : Theme.textSecondary
- font.family: Theme.fontFamilyMonospace
- font.pixelSize: Theme.fontSizeCaption
- font.weight: Theme.fontWeightMedium
- text: "[DISMISS]"
- }
-
- MouseArea {
- id: eventLogDismissMouseArea
- anchors.fill: parent
- cursorShape: Qt.PointingHandCursor
- hoverEnabled: true
- onClicked: NotificationService.dismissHistoryItem(eventLogCardItem.index)
- }
- }
- }
- }
- }
- }
-
- // Scrollbar track & thumb
- Rectangle {
- id: eventLogScrollTrack
- anchors.bottom: parent.bottom
- anchors.right: parent.right
- anchors.top: parent.top
- color: Theme.gray800
- radius: 2
- visible: eventLogHistoryListView.visible && (eventLogHistoryListView.height < eventLogHistoryListView.contentHeight)
- width: 4
-
- Rectangle {
- id: eventLogScrollThumb
- color: Theme.acidGreen
- height: Math.max(16, eventLogHistoryListView.visibleArea.heightRatio * eventLogHistoryListView.height)
- radius: 2
- width: 4
- y: eventLogHistoryListView.visibleArea.yPosition * eventLogHistoryListView.height
- }
- }
-
- // Empty state container
- Item {
- id: eventLogEmptyState
- anchors.fill: parent
- visible: NotificationService.history.count === 0
-
- ColumnLayout {
- anchors.centerIn: parent
- spacing: Theme.spacingMedium
-
- Rectangle {
- Layout.alignment: Qt.AlignHCenter
- Layout.preferredHeight: 8
- Layout.preferredWidth: 8
- color: Theme.textMuted
- radius: 4
- }
-
- Text {
- Layout.alignment: Qt.AlignHCenter
- color: Theme.textMuted
- font.family: Theme.fontFamilyMonospace
- font.pixelSize: Theme.fontSizeSmall
- font.weight: Theme.fontWeightMedium
- text: "NO NOTIFICATIONS // STANDBY"
- }
- }
- }
- }
-
- // System Logs Content Area
- Item {
- id: eventLogSystemLogsContainer
- Layout.fillWidth: true
- Layout.fillHeight: true
- visible: root.eventLogCurrentTab === 1
-
- ColumnLayout {
- anchors.centerIn: parent
- spacing: Theme.spacingMedium
-
- Rectangle {
- Layout.alignment: Qt.AlignHCenter
- Layout.preferredHeight: 8
- Layout.preferredWidth: 8
- color: Theme.textMuted
- radius: 4
- }
-
- Text {
- Layout.alignment: Qt.AlignHCenter
- color: Theme.textMuted
- font.family: Theme.fontFamilyMonospace
- font.pixelSize: Theme.fontSizeSmall
- font.weight: Theme.fontWeightMedium
- text: "SYSTEM LOGS // UNAVAILABLE"
- }
- }
- }
-
- // Footer Divider
- Rectangle {
- Layout.fillWidth: true
- Layout.preferredHeight: Theme.borderWidth
- color: Theme.borderMuted
- visible: root.eventLogCurrentTab === 0
- }
-
- // Footer Action Row
- RowLayout {
- Layout.fillWidth: true
- spacing: Theme.spacingMedium
- visible: root.eventLogCurrentTab === 0
-
- Text {
- Layout.fillWidth: true
- color: Theme.textSecondary
- font.family: Theme.fontFamilyMonospace
- font.pixelSize: Theme.fontSizeCaption
- text: NotificationService.history.count > 0
- ? NotificationService.history.count + " LOGGED"
- : "0 LOGGED"
- }
-
- Rectangle {
- id: eventLogClearAllBtn
- Layout.preferredHeight: 26
- Layout.preferredWidth: eventLogClearAllText.implicitWidth + Theme.paddingMedium * 2
- border.color: NotificationService.history.count === 0
- ? Theme.borderMuted
- : (eventLogClearMouseArea.containsMouse ? Theme.destructive : Theme.borderMuted)
- border.width: Theme.borderWidth
- color: NotificationService.history.count === 0
- ? "transparent"
- : (eventLogClearMouseArea.containsMouse ? Theme.surfaceHover : "transparent")
- opacity: NotificationService.history.count === 0 ? 0.4 : 1.0
- radius: Theme.radiusSmall
-
- Text {
- id: eventLogClearAllText
- anchors.centerIn: parent
- color: NotificationService.history.count === 0
- ? Theme.textMuted
- : (eventLogClearMouseArea.containsMouse ? Theme.destructive : Theme.textPrimary)
- font.family: Theme.fontFamilyMonospace
- font.pixelSize: Theme.fontSizeCaption
- font.weight: Theme.fontWeightBold
- text: "[CLEAR ALL]"
- }
-
- MouseArea {
- id: eventLogClearMouseArea
- anchors.fill: parent
- cursorShape: NotificationService.history.count > 0 ? Qt.PointingHandCursor : Qt.ArrowCursor
- enabled: NotificationService.history.count > 0
- hoverEnabled: NotificationService.history.count > 0
- onClicked: NotificationService.clearAll()
- }
- }
- }
- }
- }
-
// =========================================================================
// Mouse Interaction
// =========================================================================
@@ -828,30 +280,17 @@ Rectangle {
hoverEnabled: true
acceptedButtons: Qt.LeftButton | Qt.RightButton
- // Only handle clicks in compact/notification states.
- // In eventLog state, eventLogInsideClickConsumer takes precedence
- // except for this top-level area which we still need for right-click DND.
onClicked: (mouse) => {
if (mouse.button === Qt.RightButton) {
NotificationService.toggleDnd();
} else {
- if (root.state === "eventLog") {
- // Toggle off: close the event log
+ if (root.isEventLogOpen) {
OverlayController.close();
- } else if (root.state === "notification") {
- // Collapse notification banner and open event log
+ } else {
collapseTimer.stop();
root.isExpanded = false;
- root.state = "eventLog";
+ root.state = "compact";
OverlayController.openEventLog();
- } else {
- // Compact state: toggle event log
- if (root.isEventLogOpen) {
- OverlayController.close();
- } else {
- root.state = "eventLog";
- OverlayController.openEventLog();
- }
}
}
}
diff --git a/shell/docs/design/dynamic-island-event-log-morph.md b/shell/docs/design/dynamic-island-event-log-morph.md
new file mode 100644
index 00000000..1b496cb6
--- /dev/null
+++ b/shell/docs/design/dynamic-island-event-log-morph.md
@@ -0,0 +1,93 @@
+# Dynamic Island & Command Center Event Log Morph Architecture
+
+## 1. Context & Problem Statement
+
+In ctOS v1, notifications and system events are surfaced through the **Dynamic Island** in the top bar (`AmbientBar`), while the **Event Log** was initially conceived as a separate dropdown popup window below the bar.
+
+When attempting to morph the Dynamic Island by enlarging `DynamicIsland.qml` directly inside `AmbientBar.qml`, two critical Wayland compositor and Layer-Shell issues occurred:
+
+1. **Wayland Layer Conflict & Input Interception (`overlayHost` covers `AmbientBar`)**:
+ - `AmbientBar` resides on `WlrLayer.Top` (default for panels).
+ - When the Event Log overlay opens, `overlayHost` in `shell.qml` becomes visible on `WlrLayer.Overlay`.
+ - In the Wayland Layer-Shell specification, `WlrLayer.Overlay` is strictly **above** `WlrLayer.Top`.
+ - The full-screen `scrimBackdrop` MouseArea on `WlrLayer.Overlay` intercepts all pointer clicks below the top bar's 40px exclusive zone. Clicks meant for the expanded Event Log hit the scrim instead and close the surface, making only the top 40px strip clickable.
+
+2. **Per-Frame Wayland Surface Reconfiguration & Mask Thrashing**:
+ - Animating `implicitHeight` on `AmbientBar` during the 300ms transition forces Quickshell to send `zwlr_layer_surface_v1.set_size` requests to Hyprland on every animation frame.
+ - The compositor must negotiate `configure` events and reallocate/render a full-screen-width surface buffer (1920–3840px wide) 60–144 times per second.
+ - Concurrently, `centerSection.widthChanged` and `centerSection.heightChanged` triggers `flushWaylandMask()`, thrashing `wl_region` allocations and destroying frame rates.
+
+---
+
+## 2. Architectural Approaches
+
+### Approach A: The Hybrid Takeover (Selected for Implementation)
+
+#### Core Concept
+Keep `AmbientBar` lean, static, and performant at a fixed 40px height on `WlrLayer.Top`. The morphing animation and expanded Command Center are delegated to a dedicated window on **`WlrLayer.Overlay`** (`eventLogPopupHost`), positioned at the exact screen coordinates of the Dynamic Island.
+
+```text
+Idle State:
+[ Left Sections ] -------- [ Dynamic Island (120x34) ] -------- [ Right Sections ] (AmbientBar on WlrLayer.Top)
+
+Click / Open EventLog:
+1. AmbientBar's Dynamic Island pill is hidden / opacity 0.
+2. overlayHost scrim fades in on WlrLayer.Overlay.
+3. eventLogPopupHost on WlrLayer.Overlay (anchored top-center, margin-top: 3px)
+ begins animation from 120x34 down to 360x500px on the GPU.
+4. All contents (Event Log tabs, notifications, controls) are 100% interactive.
+```
+
+#### Key Architecture Specifications
+- **Window Layer**: `WlrLayershell.layer: WlrLayer.Overlay`, `exclusionMode: ExclusionMode.Ignore`.
+- **Fixed Canvas Geometry**: The `PanelWindow` has a static transparent canvas (`width: 360, height: 508`, `anchors { top: true; horizontalCenter: true }`, `margins { top: 3 }`).
+- **Zero Wayland Resizing**: The Wayland surface size remains completely static. The visual morphing happens entirely within QML via GPU rendering of the inner `Rectangle` (from 120×34 to 360×500).
+- **Zero Mask Thrashing**: No dynamic Wayland `Region` mask updates are required during animation.
+- **Full Pointer Interactivity**: Because the window lives on `WlrLayer.Overlay` above the scrim, all buttons, tabs, scrollbars, and cards receive pointer events without interference.
+
+---
+
+### Approach B: 100% Standalone Autonomous Island Window
+
+#### Core Concept
+Decouple the Dynamic Island from `AmbientBar` completely. The center of `AmbientBar` remains an empty gap, while the Dynamic Island is instantiated as an independent, floating `PanelWindow` on `WlrLayer.Overlay`.
+
+```text
+[ AmbientBar: Left Sections ] (Empty Gap) [ AmbientBar: Right Sections ]
+ │
+ [ Floating Dynamic Island ] (Autonomous Window on Overlay)
+ │
+ (Morphs down to Command Center)
+```
+
+#### Key Architecture Specifications
+- **True Floating Island**: Sits above all desktop content, full-screen video, and overlays.
+- **Multi-State Live Activities**: Can morph between:
+ - `Compact Pill` (120×34): Idle status, unread indicator, DND indicator.
+ - `Media Pill` (240×34): Mini player with album art, song title, and animated visualizer bars.
+ - `Notification Toast` (320×44): Incoming alert banner.
+ - `Command Center HUD` (380×520): Multi-tab control center.
+- **Wayland Input Considerations**: Requires managing transparent input boundaries via a single, simple `Region` mask or sizing the window dynamically between distinct discrete states when not transitioning.
+
+---
+
+## 3. Decision & Implementation Plan
+
+**Selected Strategy**: **Approach A (Hybrid Takeover)**
+
+### Step-by-Step Execution Plan:
+1. **Restore `AmbientBar.qml`**:
+ - Revert `implicitHeight` to static `Theme.barHeight` (40px).
+ - Keep `centerSection` (Dynamic Island) compact/notification pill inside the bar.
+ - When `OverlayController.activeSurface === OverlayController.Surface.EventLog`, hide or synchronize the bar's Dynamic Island pill.
+
+2. **Refactor `DynamicIsland.qml`**:
+ - Keep the clean compact (120×34) and notification (300×34) states inside the bar.
+ - On click: call `OverlayController.openEventLog()` to trigger the overlay takeover.
+
+3. **Implement Morphing in `EventLog.qml` / `eventLogPopupHost`**:
+ - In `shell.qml`, re-enable `eventLogPopupHost` on `WlrLayer.Overlay`.
+ - Anchor `eventLogPopupHost` to `top: true, horizontalCenter: true` with `margins { top: 3 }`.
+ - Set fixed transparent canvas (`width: 360, height: 508`).
+ - In `EventLog.qml`, animate the root container geometry from `width: 120, height: 34` up to `width: 360, height: 500` upon opening (and down upon closing).
+ - Cross-fade header, tabs, and content smoothly as the panel reaches its expanded dimensions.
diff --git a/shell/shell.qml b/shell/shell.qml
index e6f45858..af61f621 100644
--- a/shell/shell.qml
+++ b/shell/shell.qml
@@ -661,34 +661,34 @@ Scope {
anchors.horizontalCenter: parent.horizontalCenter
}
}
- // EventLog is now embedded in DynamicIsland — standalone popup removed
- // PanelWindow {
- // id: eventLogPopupHost
- //
- // screen: overlayHost.screen
- // color: "transparent"
- // visible: OverlayController.activeSurface === OverlayController.Surface.EventLog && overlayHost.screen !== null
- // exclusionMode: ExclusionMode.Ignore
- //
- // WlrLayershell.layer: WlrLayer.Overlay
- // WlrLayershell.keyboardFocus: WlrKeyboardFocus.OnDemand
- // WlrLayershell.namespace: "ctos-eventlog-popup"
- //
- // anchors {
- // top: true
- //
- // }
- // margins {
- // top: Settings.barHeight + Theme.spacingMedium
- // }
- //
- // implicitWidth: eventLogPopup.implicitWidth
- // implicitHeight: eventLogPopup.implicitHeight
- //
- // EventLog {
- // id: eventLogPopup
- // }
- // }
+ PanelWindow {
+ id: eventLogPopupHost
+
+ screen: overlayHost.screen
+ color: "transparent"
+ visible: OverlayController.activeSurface === OverlayController.Surface.EventLog && overlayHost.screen !== null
+ exclusionMode: ExclusionMode.Ignore
+
+ WlrLayershell.layer: WlrLayer.Overlay
+ WlrLayershell.keyboardFocus: WlrKeyboardFocus.OnDemand
+ WlrLayershell.namespace: "ctos-eventlog-popup"
+
+ anchors {
+ top: true
+ }
+ margins {
+ top: 3
+ }
+
+ implicitWidth: 360
+ implicitHeight: 508
+
+ EventLog {
+ id: eventLogPopup
+ anchors.horizontalCenter: parent.horizontalCenter
+ anchors.top: parent.top
+ }
+ }
PanelWindow {
id: calendarBackdropHost
From 23a026cba1b7585b843dcd08562f3170c9e96965 Mon Sep 17 00:00:00 2001
From: reze-dev <25588579+reze-dev@users.noreply.github.com>
Date: Tue, 29 Sep 2026 01:47:00 +0530
Subject: [PATCH 45/48] feat(ui): add 4-tab cyber command center, media live
activity, and jitter-free black border
---
shell/desktop/surfaces/EventLog.qml | 1068 ++++++++++++++++-
.../surfaces/components/DynamicIsland.qml | 188 ++-
2 files changed, 1216 insertions(+), 40 deletions(-)
diff --git a/shell/desktop/surfaces/EventLog.qml b/shell/desktop/surfaces/EventLog.qml
index 924968cf..ecbdf190 100644
--- a/shell/desktop/surfaces/EventLog.qml
+++ b/shell/desktop/surfaces/EventLog.qml
@@ -3,6 +3,7 @@ pragma ComponentBehavior: Bound
import QtQuick
import QtQuick.Layouts
import Quickshell
+import Quickshell.Services.Mpris
import "../core"
import "../services"
import "./components"
@@ -45,6 +46,90 @@ FocusScope {
}
}
+ // =========================================================================
+ // MPRIS Active Player Tracking (Tab 2 Audio Matrix)
+ // =========================================================================
+
+ property int _mprisTrigger: 0
+
+ Repeater {
+ model: Mpris.players.values
+
+ Item {
+ id: mprisWatcher
+ required property var modelData
+
+ Connections {
+ target: mprisWatcher.modelData
+
+ function onPlaybackStateChanged(): void {
+ root._mprisTrigger++;
+ }
+ function onTrackTitleChanged(): void {
+ root._mprisTrigger++;
+ }
+ function onTrackArtistChanged(): void {
+ root._mprisTrigger++;
+ }
+ }
+ }
+ }
+
+ readonly property var activePlayer: {
+ const trigger = root._mprisTrigger;
+ const list = Mpris.players.values;
+ if (!list || list.length === 0) return null;
+
+ for (let i = 0; i < list.length; i++) {
+ const p = list[i];
+ if (p && p.playbackState === MprisPlaybackState.Playing) {
+ return p;
+ }
+ }
+
+ for (let i = 0; i < list.length; i++) {
+ const p = list[i];
+ if (p && p.playbackState === MprisPlaybackState.Paused && p.trackTitle && p.trackTitle.trim() !== "") {
+ return p;
+ }
+ }
+
+ for (let i = 0; i < list.length; i++) {
+ const p = list[i];
+ if (p && p.playbackState === MprisPlaybackState.Paused) {
+ return p;
+ }
+ }
+
+ return list[0] || null;
+ }
+
+ readonly property bool isPlaying: activePlayer !== null && activePlayer.playbackState === MprisPlaybackState.Playing
+
+ property real visualizerPhase: 0.0
+
+ NumberAnimation {
+ id: visualizerAnim
+ target: root
+ property: "visualizerPhase"
+ from: 0.0
+ to: 6.283185307179586
+ duration: 1200
+ loops: Animation.Infinite
+ running: root.isOpen && root.currentTab === 2 && root.isPlaying && !Settings.reducedMotion
+ }
+
+ // Rate formatting helper for telemetry
+ function formatBytesRate(bytes: real): string {
+ if (bytes >= 1048576) {
+ return (bytes / 1048576).toFixed(1) + " MB/s";
+ }
+ if (bytes >= 1024) {
+ return (bytes / 1024).toFixed(1) + " KB/s";
+ }
+ return Math.round(bytes) + " B/s";
+ }
+
// Dismiss area covering the outer canvas outside the animated panel
MouseArea {
id: canvasDismissArea
@@ -59,9 +144,9 @@ FocusScope {
anchors.top: parent.top
width: root.isOpen ? 360 : 120
height: root.isOpen ? 500 : 34
- radius: root.isOpen ? Theme.radiusMedium : Theme.radiusPill
+ radius: root.isOpen ? Theme.radiusMedium : 17
color: Theme.background
- border.color: Theme.borderMuted
+ border.color: root.isOpen ? Theme.accent : "black"
border.width: Theme.borderWidth
clip: true
@@ -110,7 +195,8 @@ FocusScope {
Behavior on opacity {
NumberAnimation {
- duration: Settings.reducedMotion ? 0 : Theme.durationFast
+ duration: Settings.reducedMotion ? 0 : Theme.durationSlow
+ easing.type: Easing.InOutQuad
}
}
}
@@ -126,7 +212,8 @@ FocusScope {
Behavior on opacity {
NumberAnimation {
- duration: Settings.reducedMotion ? 0 : Theme.durationFast
+ duration: Settings.reducedMotion ? 0 : Theme.durationSlow
+ easing.type: Easing.InOutQuad
}
}
@@ -148,7 +235,13 @@ FocusScope {
font.family: Theme.fontFamilyMonospace
font.pixelSize: Theme.fontSizeSegment
font.weight: Theme.fontWeightBold
- text: "// EVENT LOG"
+ text: {
+ if (root.currentTab === 0) return "// EVENT LOG";
+ if (root.currentTab === 1) return "// QUICK SWITCHES";
+ if (root.currentTab === 2) return "// AUDIO MATRIX";
+ if (root.currentTab === 3) return "// SYSTEM TELEMETRY";
+ return "// EVENT LOG";
+ }
}
// DND Toggle Button
@@ -209,19 +302,20 @@ FocusScope {
}
}
- // Header Divider & Tabs
+ // Header Divider & 4 Tabs
ColumnLayout {
Layout.fillWidth: true
spacing: 0
RowLayout {
Layout.fillWidth: true
- spacing: Theme.spacingMedium
+ spacing: Theme.spacingSmall
+ // Tab 0: EVENTS
Rectangle {
id: tab0
+ Layout.fillWidth: true
Layout.preferredHeight: 24
- Layout.preferredWidth: tab0Text.implicitWidth + Theme.paddingMedium * 2
color: root.currentTab === 0 ? Theme.surfaceSelected : (tab0MouseArea.containsMouse ? Theme.surfaceHover : "transparent")
border.color: root.currentTab === 0 ? Theme.accent : Theme.borderMuted
border.width: Theme.borderWidth
@@ -234,7 +328,7 @@ FocusScope {
font.family: Theme.fontFamilyMonospace
font.pixelSize: Theme.fontSizeCaption
font.weight: Theme.fontWeightBold
- text: "NOTIFICATIONS"
+ text: "EVENTS"
}
MouseArea {
@@ -246,10 +340,11 @@ FocusScope {
}
}
+ // Tab 1: SWITCHES
Rectangle {
id: tab1
+ Layout.fillWidth: true
Layout.preferredHeight: 24
- Layout.preferredWidth: tab1Text.implicitWidth + Theme.paddingMedium * 2
color: root.currentTab === 1 ? Theme.surfaceSelected : (tab1MouseArea.containsMouse ? Theme.surfaceHover : "transparent")
border.color: root.currentTab === 1 ? Theme.accent : Theme.borderMuted
border.width: Theme.borderWidth
@@ -262,7 +357,7 @@ FocusScope {
font.family: Theme.fontFamilyMonospace
font.pixelSize: Theme.fontSizeCaption
font.weight: Theme.fontWeightBold
- text: "SYSTEM LOGS"
+ text: "SWITCHES"
}
MouseArea {
@@ -273,6 +368,64 @@ FocusScope {
onClicked: root.currentTab = 1
}
}
+
+ // Tab 2: AUDIO
+ Rectangle {
+ id: tab2
+ Layout.fillWidth: true
+ Layout.preferredHeight: 24
+ color: root.currentTab === 2 ? Theme.surfaceSelected : (tab2MouseArea.containsMouse ? Theme.surfaceHover : "transparent")
+ border.color: root.currentTab === 2 ? Theme.accent : Theme.borderMuted
+ border.width: Theme.borderWidth
+ radius: Theme.radiusSmall
+
+ Text {
+ id: tab2Text
+ anchors.centerIn: parent
+ color: root.currentTab === 2 ? Theme.accent : Theme.textSecondary
+ font.family: Theme.fontFamilyMonospace
+ font.pixelSize: Theme.fontSizeCaption
+ font.weight: Theme.fontWeightBold
+ text: "AUDIO"
+ }
+
+ MouseArea {
+ id: tab2MouseArea
+ anchors.fill: parent
+ hoverEnabled: true
+ cursorShape: Qt.PointingHandCursor
+ onClicked: root.currentTab = 2
+ }
+ }
+
+ // Tab 3: TELEMETRY
+ Rectangle {
+ id: tab3
+ Layout.fillWidth: true
+ Layout.preferredHeight: 24
+ color: root.currentTab === 3 ? Theme.surfaceSelected : (tab3MouseArea.containsMouse ? Theme.surfaceHover : "transparent")
+ border.color: root.currentTab === 3 ? Theme.accent : Theme.borderMuted
+ border.width: Theme.borderWidth
+ radius: Theme.radiusSmall
+
+ Text {
+ id: tab3Text
+ anchors.centerIn: parent
+ color: root.currentTab === 3 ? Theme.accent : Theme.textSecondary
+ font.family: Theme.fontFamilyMonospace
+ font.pixelSize: Theme.fontSizeCaption
+ font.weight: Theme.fontWeightBold
+ text: "TELEMETRY"
+ }
+
+ MouseArea {
+ id: tab3MouseArea
+ anchors.fill: parent
+ hoverEnabled: true
+ cursorShape: Qt.PointingHandCursor
+ onClicked: root.currentTab = 3
+ }
+ }
}
Item {
@@ -286,7 +439,9 @@ FocusScope {
}
}
- // Body Content Area (ListView or Empty State)
+ // =================================================================
+ // TAB 0: Notification History Content Area
+ // =================================================================
Item {
id: contentContainer
Layout.fillWidth: true
@@ -494,34 +649,885 @@ FocusScope {
}
}
- // System Logs Content Area
- Item {
- id: systemLogsContainer
+ // =================================================================
+ // TAB 1: Quick Switches Content Area
+ // =================================================================
+ ColumnLayout {
+ id: quickSwitchesContainer
Layout.fillWidth: true
Layout.fillHeight: true
visible: root.currentTab === 1
+ spacing: Theme.spacingMedium
- ColumnLayout {
- anchors.centerIn: parent
- spacing: Theme.spacingMedium
+ // Switch 1: Do Not Disturb
+ Rectangle {
+ Layout.fillWidth: true
+ Layout.preferredHeight: 64
+ color: NotificationService.doNotDisturb ? Theme.surfaceSelected : Theme.gray800
+ border.color: NotificationService.doNotDisturb ? Theme.warningRed : (swDndMouse.containsMouse ? Theme.accent : Theme.gray700)
+ border.width: Theme.borderWidth
+ radius: Theme.radiusSmall
- Rectangle {
- Layout.alignment: Qt.AlignHCenter
- Layout.preferredHeight: 8
- Layout.preferredWidth: 8
- color: Theme.textMuted
- radius: 4
+ RowLayout {
+ anchors.fill: parent
+ anchors.margins: Theme.paddingMedium
+ spacing: Theme.spacingMedium
+
+ Rectangle {
+ Layout.preferredWidth: 8
+ Layout.preferredHeight: 8
+ radius: 4
+ color: NotificationService.doNotDisturb ? Theme.warningRed : Theme.gray600
+ }
+
+ ColumnLayout {
+ Layout.fillWidth: true
+ spacing: 2
+
+ Text {
+ text: "DO NOT DISTURB"
+ color: NotificationService.doNotDisturb ? Theme.warningRed : Theme.textPrimary
+ font.family: Theme.fontFamilyMonospace
+ font.pixelSize: Theme.fontSizeSmall
+ font.weight: Theme.fontWeightBold
+ }
+
+ Text {
+ text: NotificationService.doNotDisturb ? "ACTIVE // TOASTS SUPPRESSED" : "INACTIVE // TOASTS ALLOWED"
+ color: Theme.textMuted
+ font.family: Theme.fontFamilyMonospace
+ font.pixelSize: Theme.fontSizeCaption
+ }
+ }
+
+ Rectangle {
+ Layout.preferredWidth: 70
+ Layout.preferredHeight: 24
+ color: NotificationService.doNotDisturb ? Theme.warningRed : (swDndMouse.containsMouse ? Theme.surfaceHover : "transparent")
+ border.color: NotificationService.doNotDisturb ? Theme.warningRed : Theme.borderMuted
+ border.width: Theme.borderWidth
+ radius: Theme.radiusSmall
+
+ Text {
+ anchors.centerIn: parent
+ text: NotificationService.doNotDisturb ? "MUTED" : "OFF"
+ color: NotificationService.doNotDisturb ? "black" : Theme.textSecondary
+ font.family: Theme.fontFamilyMonospace
+ font.pixelSize: Theme.fontSizeCaption
+ font.weight: Theme.fontWeightBold
+ }
+ }
}
- Text {
- Layout.alignment: Qt.AlignHCenter
- color: Theme.textMuted
- font.family: Theme.fontFamilyMonospace
- font.pixelSize: Theme.fontSizeSmall
- font.weight: Theme.fontWeightMedium
- text: "SYSTEM LOGS // UNAVAILABLE"
+ MouseArea {
+ id: swDndMouse
+ anchors.fill: parent
+ cursorShape: Qt.PointingHandCursor
+ hoverEnabled: true
+ onClicked: NotificationService.toggleDnd()
+ }
+ }
+
+ // Switch 2: Wi-Fi Subsystem
+ Rectangle {
+ Layout.fillWidth: true
+ Layout.preferredHeight: 64
+ color: NetworkService.wifiEnabled ? Theme.surfaceSelected : Theme.gray800
+ border.color: NetworkService.wifiEnabled ? Theme.acidGreen : (swWifiMouse.containsMouse ? Theme.accent : Theme.gray700)
+ border.width: Theme.borderWidth
+ radius: Theme.radiusSmall
+
+ RowLayout {
+ anchors.fill: parent
+ anchors.margins: Theme.paddingMedium
+ spacing: Theme.spacingMedium
+
+ Rectangle {
+ Layout.preferredWidth: 8
+ Layout.preferredHeight: 8
+ radius: 4
+ color: NetworkService.wifiEnabled ? Theme.acidGreen : Theme.gray600
+ }
+
+ ColumnLayout {
+ Layout.fillWidth: true
+ spacing: 2
+
+ Text {
+ text: "WI-FI SUBSYSTEM"
+ color: NetworkService.wifiEnabled ? Theme.acidGreen : Theme.textPrimary
+ font.family: Theme.fontFamilyMonospace
+ font.pixelSize: Theme.fontSizeSmall
+ font.weight: Theme.fontWeightBold
+ }
+
+ Text {
+ text: NetworkService.wifiEnabled
+ ? (NetworkService.isWifi ? ("CONNECTED: " + NetworkService.networkName) : "RADIO ON // DISCONNECTED")
+ : "RADIO DISABLED"
+ color: Theme.textMuted
+ font.family: Theme.fontFamilyMonospace
+ font.pixelSize: Theme.fontSizeCaption
+ elide: Text.ElideRight
+ Layout.fillWidth: true
+ }
+ }
+
+ Rectangle {
+ Layout.preferredWidth: 70
+ Layout.preferredHeight: 24
+ color: NetworkService.wifiEnabled ? Theme.acidGreen : (swWifiMouse.containsMouse ? Theme.surfaceHover : "transparent")
+ border.color: NetworkService.wifiEnabled ? Theme.acidGreen : Theme.borderMuted
+ border.width: Theme.borderWidth
+ radius: Theme.radiusSmall
+
+ Text {
+ anchors.centerIn: parent
+ text: NetworkService.wifiEnabled ? "ON" : "OFF"
+ color: NetworkService.wifiEnabled ? "black" : Theme.textSecondary
+ font.family: Theme.fontFamilyMonospace
+ font.pixelSize: Theme.fontSizeCaption
+ font.weight: Theme.fontWeightBold
+ }
+ }
+ }
+
+ MouseArea {
+ id: swWifiMouse
+ anchors.fill: parent
+ cursorShape: Qt.PointingHandCursor
+ hoverEnabled: true
+ onClicked: NetworkService.toggleWifi()
+ }
+ }
+
+ // Switch 3: Bluetooth Subsystem
+ Rectangle {
+ Layout.fillWidth: true
+ Layout.preferredHeight: 64
+ color: BluetoothService.powered ? Theme.surfaceSelected : Theme.gray800
+ border.color: BluetoothService.powered ? Theme.acidGreen : (swBtMouse.containsMouse ? Theme.accent : Theme.gray700)
+ border.width: Theme.borderWidth
+ radius: Theme.radiusSmall
+
+ RowLayout {
+ anchors.fill: parent
+ anchors.margins: Theme.paddingMedium
+ spacing: Theme.spacingMedium
+
+ Rectangle {
+ Layout.preferredWidth: 8
+ Layout.preferredHeight: 8
+ radius: 4
+ color: BluetoothService.powered ? Theme.acidGreen : Theme.gray600
+ }
+
+ ColumnLayout {
+ Layout.fillWidth: true
+ spacing: 2
+
+ Text {
+ text: "BLUETOOTH LINK"
+ color: BluetoothService.powered ? Theme.acidGreen : Theme.textPrimary
+ font.family: Theme.fontFamilyMonospace
+ font.pixelSize: Theme.fontSizeSmall
+ font.weight: Theme.fontWeightBold
+ }
+
+ Text {
+ text: BluetoothService.powered
+ ? (BluetoothService.isConnected ? ("LINKED: " + BluetoothService.deviceName) : "ADAPTER ACTIVE // IDLE")
+ : "ADAPTER POWERED OFF"
+ color: Theme.textMuted
+ font.family: Theme.fontFamilyMonospace
+ font.pixelSize: Theme.fontSizeCaption
+ elide: Text.ElideRight
+ Layout.fillWidth: true
+ }
+ }
+
+ Rectangle {
+ Layout.preferredWidth: 70
+ Layout.preferredHeight: 24
+ color: BluetoothService.powered ? Theme.acidGreen : (swBtMouse.containsMouse ? Theme.surfaceHover : "transparent")
+ border.color: BluetoothService.powered ? Theme.acidGreen : Theme.borderMuted
+ border.width: Theme.borderWidth
+ radius: Theme.radiusSmall
+
+ Text {
+ anchors.centerIn: parent
+ text: BluetoothService.powered ? "ON" : "OFF"
+ color: BluetoothService.powered ? "black" : Theme.textSecondary
+ font.family: Theme.fontFamilyMonospace
+ font.pixelSize: Theme.fontSizeCaption
+ font.weight: Theme.fontWeightBold
+ }
+ }
+ }
+
+ MouseArea {
+ id: swBtMouse
+ anchors.fill: parent
+ cursorShape: Qt.PointingHandCursor
+ hoverEnabled: true
+ onClicked: BluetoothService.togglePower()
}
}
+
+ // Switch 4: Master Audio Mute
+ Rectangle {
+ Layout.fillWidth: true
+ Layout.preferredHeight: 64
+ color: !AudioService.muted ? Theme.surfaceSelected : Theme.gray800
+ border.color: !AudioService.muted ? Theme.acidGreen : (swAudioMouse.containsMouse ? Theme.accent : Theme.gray700)
+ border.width: Theme.borderWidth
+ radius: Theme.radiusSmall
+
+ RowLayout {
+ anchors.fill: parent
+ anchors.margins: Theme.paddingMedium
+ spacing: Theme.spacingMedium
+
+ Rectangle {
+ Layout.preferredWidth: 8
+ Layout.preferredHeight: 8
+ radius: 4
+ color: !AudioService.muted ? Theme.acidGreen : Theme.warningRed
+ }
+
+ ColumnLayout {
+ Layout.fillWidth: true
+ spacing: 2
+
+ Text {
+ text: "MASTER AUDIO SINK"
+ color: !AudioService.muted ? Theme.acidGreen : Theme.textPrimary
+ font.family: Theme.fontFamilyMonospace
+ font.pixelSize: Theme.fontSizeSmall
+ font.weight: Theme.fontWeightBold
+ }
+
+ Text {
+ text: AudioService.muted
+ ? "MUTED // SILENT"
+ : ("ACTIVE // " + Math.round(AudioService.volume * 100) + "%" + (AudioService.sinkName !== "" ? (" (" + AudioService.sinkName + ")") : ""))
+ color: Theme.textMuted
+ font.family: Theme.fontFamilyMonospace
+ font.pixelSize: Theme.fontSizeCaption
+ elide: Text.ElideRight
+ Layout.fillWidth: true
+ }
+ }
+
+ Rectangle {
+ Layout.preferredWidth: 70
+ Layout.preferredHeight: 24
+ color: AudioService.muted ? Theme.warningRed : (swAudioMouse.containsMouse ? Theme.surfaceHover : "transparent")
+ border.color: AudioService.muted ? Theme.warningRed : Theme.acidGreen
+ border.width: Theme.borderWidth
+ radius: Theme.radiusSmall
+
+ Text {
+ anchors.centerIn: parent
+ text: AudioService.muted ? "MUTED" : "UNMUTED"
+ color: AudioService.muted ? "black" : Theme.acidGreen
+ font.family: Theme.fontFamilyMonospace
+ font.pixelSize: Theme.fontSizeCaption
+ font.weight: Theme.fontWeightBold
+ }
+ }
+ }
+
+ MouseArea {
+ id: swAudioMouse
+ anchors.fill: parent
+ cursorShape: Qt.PointingHandCursor
+ hoverEnabled: true
+ onClicked: AudioService.toggleMute()
+ }
+ }
+
+ Item {
+ Layout.fillHeight: true
+ }
+ }
+
+ // =================================================================
+ // TAB 2: Audio Matrix Content Area
+ // =================================================================
+ ColumnLayout {
+ id: audioMatrixContainer
+ Layout.fillWidth: true
+ Layout.fillHeight: true
+ visible: root.currentTab === 2
+ spacing: Theme.spacingMedium
+
+ // Output Volume Card
+ Rectangle {
+ Layout.fillWidth: true
+ Layout.preferredHeight: 88
+ color: Theme.gray800
+ border.color: Theme.gray700
+ border.width: Theme.borderWidth
+ radius: Theme.radiusSmall
+
+ ColumnLayout {
+ anchors.fill: parent
+ anchors.margins: Theme.paddingMedium
+ spacing: Theme.spacingSmall
+
+ RowLayout {
+ Layout.fillWidth: true
+
+ Text {
+ text: "MASTER OUTPUT"
+ color: Theme.textSecondary
+ font.family: Theme.fontFamilyMonospace
+ font.pixelSize: Theme.fontSizeCaption
+ font.weight: Theme.fontWeightBold
+ }
+
+ Item {
+ Layout.fillWidth: true
+ }
+
+ Text {
+ text: AudioService.muted
+ ? "MUTED"
+ : Math.round(AudioService.volume * 100) + "%"
+ color: AudioService.muted ? Theme.warningRed : Theme.acidGreen
+ font.family: Theme.fontFamilyMonospace
+ font.pixelSize: Theme.fontSizeCaption
+ font.weight: Theme.fontWeightBold
+ }
+ }
+
+ // Slider Track
+ Rectangle {
+ id: sliderTrack
+ Layout.fillWidth: true
+ Layout.preferredHeight: 12
+ color: Theme.gray700
+ radius: 6
+
+ Rectangle {
+ id: sliderFill
+ width: Math.max(0, Math.min(sliderTrack.width, sliderTrack.width * AudioService.volume))
+ height: parent.height
+ radius: 6
+ color: AudioService.muted ? Theme.warningRed : Theme.acidGreen
+ }
+
+ // Slider thumb handle
+ Rectangle {
+ x: Math.max(0, Math.min(sliderTrack.width - width, sliderTrack.width * AudioService.volume - width / 2))
+ anchors.verticalCenter: parent.verticalCenter
+ width: 16
+ height: 16
+ radius: 8
+ color: Theme.gray50
+ border.color: AudioService.muted ? Theme.warningRed : Theme.acidGreen
+ border.width: 2
+ visible: volumeMouseArea.containsMouse || volumeMouseArea.pressed
+ }
+
+ MouseArea {
+ id: volumeMouseArea
+ anchors.fill: parent
+ cursorShape: Qt.PointingHandCursor
+ hoverEnabled: true
+
+ function applyVolume(mouse): void {
+ const v = Math.max(0.0, Math.min(1.0, mouse.x / sliderTrack.width));
+ AudioService.setVolume(v);
+ }
+
+ onPressed: (mouse) => applyVolume(mouse)
+ onPositionChanged: (mouse) => {
+ if (pressed) applyVolume(mouse);
+ }
+ }
+ }
+
+ // Bottom row: Sink name & Mute button
+ RowLayout {
+ Layout.fillWidth: true
+
+ Text {
+ Layout.fillWidth: true
+ text: AudioService.sinkName !== "" ? AudioService.sinkName : "DEFAULT SINK"
+ color: Theme.textMuted
+ font.family: Theme.fontFamilyMonospace
+ font.pixelSize: 10
+ elide: Text.ElideRight
+ }
+
+ Rectangle {
+ Layout.preferredHeight: 20
+ Layout.preferredWidth: 60
+ color: AudioService.muted ? Theme.warningRed : (muteBtnMouse.containsMouse ? Theme.surfaceHover : "transparent")
+ border.color: AudioService.muted ? Theme.warningRed : Theme.borderMuted
+ border.width: Theme.borderWidth
+ radius: Theme.radiusSmall
+
+ Text {
+ anchors.centerIn: parent
+ text: AudioService.muted ? "UNMUTE" : "MUTE"
+ color: AudioService.muted ? "black" : Theme.textSecondary
+ font.family: Theme.fontFamilyMonospace
+ font.pixelSize: 10
+ font.weight: Theme.fontWeightBold
+ }
+
+ MouseArea {
+ id: muteBtnMouse
+ anchors.fill: parent
+ cursorShape: Qt.PointingHandCursor
+ hoverEnabled: true
+ onClicked: AudioService.toggleMute()
+ }
+ }
+ }
+ }
+ }
+
+ // MPRIS Media Card
+ Rectangle {
+ Layout.fillWidth: true
+ Layout.preferredHeight: 140
+ color: Theme.gray800
+ border.color: root.isPlaying ? Theme.acidGreen : Theme.gray700
+ border.width: Theme.borderWidth
+ radius: Theme.radiusSmall
+
+ ColumnLayout {
+ anchors.fill: parent
+ anchors.margins: Theme.paddingMedium
+ spacing: Theme.spacingSmall
+
+ RowLayout {
+ Layout.fillWidth: true
+
+ Text {
+ text: "MEDIA STREAM"
+ color: Theme.textSecondary
+ font.family: Theme.fontFamilyMonospace
+ font.pixelSize: Theme.fontSizeCaption
+ font.weight: Theme.fontWeightBold
+ }
+
+ Item {
+ Layout.fillWidth: true
+ }
+
+ Text {
+ text: root.isPlaying ? "STREAMING" : (root.activePlayer ? "PAUSED" : "OFFLINE")
+ color: root.isPlaying ? Theme.acidGreen : Theme.textMuted
+ font.family: Theme.fontFamilyMonospace
+ font.pixelSize: Theme.fontSizeCaption
+ font.weight: Theme.fontWeightBold
+ }
+ }
+
+ // Track Title
+ Text {
+ Layout.fillWidth: true
+ text: (root.activePlayer && root.activePlayer.trackTitle && root.activePlayer.trackTitle.trim() !== "")
+ ? root.activePlayer.trackTitle.trim()
+ : "NO ACTIVE MEDIA STREAM"
+ color: Theme.textPrimary
+ font.family: Theme.fontFamilyMonospace
+ font.pixelSize: Theme.fontSizeSmall
+ font.weight: Theme.fontWeightBold
+ elide: Text.ElideRight
+ }
+
+ // Artist & Source
+ Text {
+ Layout.fillWidth: true
+ text: {
+ const artist = (root.activePlayer && root.activePlayer.trackArtist) ? root.activePlayer.trackArtist.trim() : "";
+ const identity = (root.activePlayer && root.activePlayer.identity) ? root.activePlayer.identity.trim() : "";
+ if (artist !== "" && identity !== "") return artist + " // " + identity;
+ if (artist !== "") return artist;
+ if (identity !== "") return identity;
+ return "AWAITING MPRIS TRANSMISSION";
+ }
+ color: Theme.textMuted
+ font.family: Theme.fontFamilyMonospace
+ font.pixelSize: Theme.fontSizeCaption
+ elide: Text.ElideRight
+ }
+
+ // Control Buttons: Prev, Play/Pause, Next
+ RowLayout {
+ Layout.fillWidth: true
+ Layout.alignment: Qt.AlignHCenter
+ spacing: Theme.spacingMedium
+
+ // Previous Button
+ Rectangle {
+ Layout.preferredHeight: 28
+ Layout.preferredWidth: 44
+ color: prevMouse.containsMouse ? Theme.surfaceHover : "transparent"
+ border.color: prevMouse.containsMouse ? Theme.accent : Theme.borderMuted
+ border.width: Theme.borderWidth
+ radius: Theme.radiusSmall
+
+ Text {
+ anchors.centerIn: parent
+ text: "|<<"
+ color: prevMouse.containsMouse ? Theme.accent : Theme.textSecondary
+ font.family: Theme.fontFamilyMonospace
+ font.pixelSize: Theme.fontSizeCaption
+ font.weight: Theme.fontWeightBold
+ }
+
+ MouseArea {
+ id: prevMouse
+ anchors.fill: parent
+ cursorShape: Qt.PointingHandCursor
+ hoverEnabled: true
+ onClicked: {
+ if (root.activePlayer && typeof root.activePlayer.previous === "function") {
+ root.activePlayer.previous();
+ }
+ }
+ }
+ }
+
+ // Play / Pause Button
+ Rectangle {
+ Layout.preferredHeight: 28
+ Layout.preferredWidth: 80
+ color: root.isPlaying ? Theme.surfaceSelected : (playMouse.containsMouse ? Theme.surfaceHover : "transparent")
+ border.color: root.isPlaying ? Theme.acidGreen : (playMouse.containsMouse ? Theme.accent : Theme.borderMuted)
+ border.width: Theme.borderWidth
+ radius: Theme.radiusSmall
+
+ Text {
+ anchors.centerIn: parent
+ text: root.isPlaying ? "PAUSE" : "PLAY"
+ color: root.isPlaying ? Theme.acidGreen : (playMouse.containsMouse ? Theme.accent : Theme.textSecondary)
+ font.family: Theme.fontFamilyMonospace
+ font.pixelSize: Theme.fontSizeCaption
+ font.weight: Theme.fontWeightBold
+ }
+
+ MouseArea {
+ id: playMouse
+ anchors.fill: parent
+ cursorShape: Qt.PointingHandCursor
+ hoverEnabled: true
+ onClicked: {
+ if (root.activePlayer && typeof root.activePlayer.togglePlaying === "function") {
+ root.activePlayer.togglePlaying();
+ }
+ }
+ }
+ }
+
+ // Next Button
+ Rectangle {
+ Layout.preferredHeight: 28
+ Layout.preferredWidth: 44
+ color: nextMouse.containsMouse ? Theme.surfaceHover : "transparent"
+ border.color: nextMouse.containsMouse ? Theme.accent : Theme.borderMuted
+ border.width: Theme.borderWidth
+ radius: Theme.radiusSmall
+
+ Text {
+ anchors.centerIn: parent
+ text: ">>|"
+ color: nextMouse.containsMouse ? Theme.accent : Theme.textSecondary
+ font.family: Theme.fontFamilyMonospace
+ font.pixelSize: Theme.fontSizeCaption
+ font.weight: Theme.fontWeightBold
+ }
+
+ MouseArea {
+ id: nextMouse
+ anchors.fill: parent
+ cursorShape: Qt.PointingHandCursor
+ hoverEnabled: true
+ onClicked: {
+ if (root.activePlayer && typeof root.activePlayer.next === "function") {
+ root.activePlayer.next();
+ }
+ }
+ }
+ }
+ }
+
+ // Equalizer bars row
+ RowLayout {
+ Layout.fillWidth: true
+ Layout.preferredHeight: 16
+ spacing: 3
+
+ Repeater {
+ model: 12
+
+ Rectangle {
+ id: matrixEqBar
+ required property int index
+ Layout.fillWidth: true
+ Layout.alignment: Qt.AlignBottom
+ Layout.preferredHeight: root.isPlaying
+ ? Math.max(3, Math.round((Math.sin((matrixEqBar.index * 0.8) + root.visualizerPhase) * 0.4 + 0.5) * 14))
+ : 2
+ color: root.isPlaying ? Theme.acidGreen : Theme.gray600
+ radius: 1
+ }
+ }
+ }
+ }
+ }
+
+ Item {
+ Layout.fillHeight: true
+ }
+ }
+
+ // =================================================================
+ // TAB 3: System Telemetry Content Area
+ // =================================================================
+ ColumnLayout {
+ id: telemetryContainer
+ Layout.fillWidth: true
+ Layout.fillHeight: true
+ visible: root.currentTab === 3
+ spacing: Theme.spacingMedium
+
+ // Card 1: CPU Telemetry
+ Rectangle {
+ Layout.fillWidth: true
+ Layout.preferredHeight: 80
+ color: Theme.gray800
+ border.color: Theme.gray700
+ border.width: Theme.borderWidth
+ radius: Theme.radiusSmall
+
+ ColumnLayout {
+ anchors.fill: parent
+ anchors.margins: Theme.paddingMedium
+ spacing: 4
+
+ RowLayout {
+ Layout.fillWidth: true
+
+ Text {
+ text: "CPU TOTAL LOAD"
+ color: Theme.textSecondary
+ font.family: Theme.fontFamilyMonospace
+ font.pixelSize: Theme.fontSizeCaption
+ font.weight: Theme.fontWeightBold
+ }
+
+ Item {
+ Layout.fillWidth: true
+ }
+
+ Text {
+ text: Math.round(SystemMonitorService.cpuTotal * 100) + "%"
+ color: SystemMonitorService.cpuTotal > 0.85 ? Theme.warningRed : Theme.acidGreen
+ font.family: Theme.fontFamilyMonospace
+ font.pixelSize: Theme.fontSizeCaption
+ font.weight: Theme.fontWeightBold
+ }
+ }
+
+ // CPU Meter Bar
+ Rectangle {
+ Layout.fillWidth: true
+ Layout.preferredHeight: 8
+ color: Theme.gray700
+ radius: 4
+
+ Rectangle {
+ width: Math.max(0, Math.min(parent.width, parent.width * SystemMonitorService.cpuTotal))
+ height: parent.height
+ radius: 4
+ color: SystemMonitorService.cpuTotal > 0.85 ? Theme.warningRed : Theme.acidGreen
+ }
+ }
+
+ Text {
+ text: SystemMonitorService.cpuThreadLoads.length > 0
+ ? (SystemMonitorService.cpuThreadLoads.length + " THREADS MONITORED // PROCFS ACTIVE")
+ : "SYSTEM CORES ONLINE"
+ color: Theme.textMuted
+ font.family: Theme.fontFamilyMonospace
+ font.pixelSize: 10
+ }
+ }
+ }
+
+ // Card 2: Memory Allocation
+ Rectangle {
+ id: memCard
+ Layout.fillWidth: true
+ Layout.preferredHeight: 80
+ color: Theme.gray800
+ border.color: Theme.gray700
+ border.width: Theme.borderWidth
+ radius: Theme.radiusSmall
+
+ readonly property real memPercent: SystemMonitorService.memTotalBytes > 0
+ ? (SystemMonitorService.memUsedBytes / SystemMonitorService.memTotalBytes)
+ : 0.0
+
+ ColumnLayout {
+ anchors.fill: parent
+ anchors.margins: Theme.paddingMedium
+ spacing: 4
+
+ RowLayout {
+ Layout.fillWidth: true
+
+ Text {
+ text: "MEMORY USAGE"
+ color: Theme.textSecondary
+ font.family: Theme.fontFamilyMonospace
+ font.pixelSize: Theme.fontSizeCaption
+ font.weight: Theme.fontWeightBold
+ }
+
+ Item {
+ Layout.fillWidth: true
+ }
+
+ Text {
+ text: Math.round(memCard.memPercent * 100) + "%"
+ color: memCard.memPercent > 0.9 ? Theme.warningRed : Theme.acidGreen
+ font.family: Theme.fontFamilyMonospace
+ font.pixelSize: Theme.fontSizeCaption
+ font.weight: Theme.fontWeightBold
+ }
+ }
+
+ // Memory Meter Bar
+ Rectangle {
+ Layout.fillWidth: true
+ Layout.preferredHeight: 8
+ color: Theme.gray700
+ radius: 4
+
+ Rectangle {
+ width: Math.max(0, Math.min(parent.width, parent.width * memCard.memPercent))
+ height: parent.height
+ radius: 4
+ color: memCard.memPercent > 0.9 ? Theme.warningRed : Theme.acidGreen
+ }
+ }
+
+ Text {
+ text: (SystemMonitorService.memUsedBytes / 1073741824).toFixed(1) + " GB / " +
+ (SystemMonitorService.memTotalBytes / 1073741824).toFixed(1) + " GB (" +
+ (SystemMonitorService.swapUsedBytes / 1073741824).toFixed(1) + " GB SWAP)"
+ color: Theme.textMuted
+ font.family: Theme.fontFamilyMonospace
+ font.pixelSize: 10
+ }
+ }
+ }
+
+ // Card 3: Network Bandwidth Flow
+ Rectangle {
+ Layout.fillWidth: true
+ Layout.preferredHeight: 88
+ color: Theme.gray800
+ border.color: Theme.gray700
+ border.width: Theme.borderWidth
+ radius: Theme.radiusSmall
+
+ ColumnLayout {
+ anchors.fill: parent
+ anchors.margins: Theme.paddingMedium
+ spacing: 4
+
+ RowLayout {
+ Layout.fillWidth: true
+
+ Text {
+ text: "NETWORK FLOW"
+ color: Theme.textSecondary
+ font.family: Theme.fontFamilyMonospace
+ font.pixelSize: Theme.fontSizeCaption
+ font.weight: Theme.fontWeightBold
+ }
+
+ Item {
+ Layout.fillWidth: true
+ }
+
+ Text {
+ text: NetworkService.connectionType.toUpperCase()
+ color: NetworkService.isConnected ? Theme.acidGreen : Theme.textMuted
+ font.family: Theme.fontFamilyMonospace
+ font.pixelSize: Theme.fontSizeCaption
+ font.weight: Theme.fontWeightBold
+ }
+ }
+
+ RowLayout {
+ Layout.fillWidth: true
+
+ ColumnLayout {
+ Layout.fillWidth: true
+ spacing: 2
+
+ Text {
+ text: "RX FLOW"
+ color: Theme.textMuted
+ font.family: Theme.fontFamilyMonospace
+ font.pixelSize: 10
+ }
+
+ Text {
+ text: root.formatBytesRate(SystemMonitorService.netRxBytesPerSec)
+ color: Theme.acidGreen
+ font.family: Theme.fontFamilyMonospace
+ font.pixelSize: Theme.fontSizeCaption
+ font.weight: Theme.fontWeightBold
+ }
+ }
+
+ ColumnLayout {
+ Layout.fillWidth: true
+ spacing: 2
+
+ Text {
+ text: "TX FLOW"
+ color: Theme.textMuted
+ font.family: Theme.fontFamilyMonospace
+ font.pixelSize: 10
+ }
+
+ Text {
+ text: root.formatBytesRate(SystemMonitorService.netTxBytesPerSec)
+ color: Theme.pastelBlue
+ font.family: Theme.fontFamilyMonospace
+ font.pixelSize: Theme.fontSizeCaption
+ font.weight: Theme.fontWeightBold
+ }
+ }
+ }
+
+ Text {
+ text: "INTERFACE: " + NetworkService.networkName
+ color: Theme.textMuted
+ font.family: Theme.fontFamilyMonospace
+ font.pixelSize: 10
+ elide: Text.ElideRight
+ Layout.fillWidth: true
+ }
+ }
+ }
+
+ Item {
+ Layout.fillHeight: true
+ }
}
// Footer Divider
diff --git a/shell/desktop/surfaces/components/DynamicIsland.qml b/shell/desktop/surfaces/components/DynamicIsland.qml
index 4239b235..6a53cdf0 100644
--- a/shell/desktop/surfaces/components/DynamicIsland.qml
+++ b/shell/desktop/surfaces/components/DynamicIsland.qml
@@ -3,6 +3,7 @@ pragma ComponentBehavior: Bound
import QtQuick
import QtQuick.Layouts
import Quickshell
+import Quickshell.Services.Mpris
import "../../core"
import "../../services"
@@ -14,9 +15,10 @@ Rectangle {
// =========================================================================
readonly property real compactWidth: 120
+ readonly property real mediaWidth: 240
readonly property real expandedWidth: 300
- // Two primary states: "compact", "notification"
+ // States: "compact", "media", "notification"
property string state: "compact"
// Writable isExpanded for backward compatibility with tests.
@@ -29,12 +31,96 @@ Rectangle {
property int latestUrgency: 1
readonly property bool isEventLogOpen: OverlayController.activeSurface === OverlayController.Surface.EventLog
+ // =========================================================================
+ // MPRIS Active Player Tracking & Equalizer State
+ // =========================================================================
+
+ property int _mprisTrigger: 0
+
+ Repeater {
+ model: Mpris.players.values
+
+ Item {
+ id: mprisWatcher
+ required property var modelData
+
+ Connections {
+ target: mprisWatcher.modelData
+
+ function onPlaybackStateChanged(): void {
+ root._mprisTrigger++;
+ }
+ function onTrackTitleChanged(): void {
+ root._mprisTrigger++;
+ }
+ function onTrackArtistChanged(): void {
+ root._mprisTrigger++;
+ }
+ }
+ }
+ }
+
+ readonly property var activePlayer: {
+ const trigger = root._mprisTrigger;
+ const list = Mpris.players.values;
+ if (!list || list.length === 0) return null;
+
+ for (let i = 0; i < list.length; i++) {
+ const p = list[i];
+ if (p && p.playbackState === MprisPlaybackState.Playing) {
+ return p;
+ }
+ }
+
+ for (let i = 0; i < list.length; i++) {
+ const p = list[i];
+ if (p && p.playbackState === MprisPlaybackState.Paused && p.trackTitle && p.trackTitle.trim() !== "") {
+ return p;
+ }
+ }
+
+ for (let i = 0; i < list.length; i++) {
+ const p = list[i];
+ if (p && p.playbackState === MprisPlaybackState.Paused) {
+ return p;
+ }
+ }
+
+ return list[0] || null;
+ }
+
+ readonly property bool hasMedia: activePlayer !== null && (
+ activePlayer.playbackState === MprisPlaybackState.Playing ||
+ (activePlayer.trackTitle !== undefined && activePlayer.trackTitle !== null && activePlayer.trackTitle.trim() !== "")
+ )
+ readonly property bool isPlaying: hasMedia && activePlayer.playbackState === MprisPlaybackState.Playing
+
+ readonly property string mediaTitle: (activePlayer && activePlayer.trackTitle && activePlayer.trackTitle.trim() !== "") ? activePlayer.trackTitle.trim() : "--UNTITLED--"
+ readonly property string mediaArtist: (activePlayer && activePlayer.trackArtist && activePlayer.trackArtist.trim() !== "") ? activePlayer.trackArtist.trim() : ""
+
+ property real visualizerPhase: 0.0
+
+ NumberAnimation {
+ id: visualizerAnim
+ target: root
+ property: "visualizerPhase"
+ from: 0.0
+ to: 6.283185307179586
+ duration: 1200
+ loops: Animation.Infinite
+ running: root.isPlaying && !Settings.reducedMotion
+ }
+
+ // =========================================================================
+ // State Synchronization
+ // =========================================================================
+
// Sync isExpanded writes -> state
onIsExpandedChanged: {
- if (root.isExpanded && root.state === "compact") {
+ if (root.isExpanded && root.state !== "notification") {
root.state = "notification";
} else if (!root.isExpanded && root.state === "notification") {
- root.state = "compact";
+ root.state = root.hasMedia ? "media" : "compact";
}
}
@@ -42,17 +128,32 @@ Rectangle {
onStateChanged: {
if (root.state === "notification" && !root.isExpanded) {
root.isExpanded = true;
- } else if (root.state === "compact" && root.isExpanded) {
+ } else if (root.state !== "notification" && root.isExpanded) {
root.isExpanded = false;
}
}
+ // Sync media presence -> state (when not displaying notification)
+ onHasMediaChanged: {
+ if (!root.isExpanded) {
+ root.state = root.hasMedia ? "media" : "compact";
+ }
+ }
+
// Centralized layout function per dynamic-radial-geometry skill:
// Synchronously compute width/height based on state to avoid desync.
function _computeLayout(): var {
const isExp = root.isExpanded || root.state === "notification";
+ let targetW = root.compactWidth;
+ if (isExp) {
+ targetW = root.expandedWidth;
+ } else if (root.hasMedia) {
+ targetW = root.mediaWidth;
+ } else if (mouseArea.containsMouse) {
+ targetW = root.compactWidth + 12;
+ }
return {
- w: isExp ? root.expandedWidth : root.compactWidth,
+ w: targetW,
h: Theme.barHeight - 6
};
}
@@ -66,7 +167,7 @@ Rectangle {
color: Theme.background
radius: Theme.radiusPill
- border.color: mouseArea.containsMouse ? Theme.accent : Theme.borderMuted
+ border.color: (mouseArea.containsMouse || root.isExpanded) ? Theme.accent : "black"
border.width: Theme.borderWidth
clip: true
@@ -139,13 +240,13 @@ Rectangle {
}
// =========================================================================
- // Compact View
+ // Compact View (Idle / DND / Unread)
// =========================================================================
RowLayout {
id: compactContent
anchors.centerIn: parent
- opacity: (!root.isExpanded && root.state === "compact") ? 1.0 : 0.0
+ opacity: (!root.isExpanded && !root.hasMedia) ? 1.0 : 0.0
visible: opacity > 0.0
spacing: Theme.spacingSmall
@@ -209,6 +310,70 @@ Rectangle {
}
}
+ // =========================================================================
+ // Media Pill / Live Activity View (240x34)
+ // =========================================================================
+
+ RowLayout {
+ id: mediaContent
+ anchors.fill: parent
+ anchors.leftMargin: Theme.paddingLarge
+ anchors.rightMargin: Theme.paddingLarge
+ opacity: (!root.isExpanded && root.hasMedia) ? 1.0 : 0.0
+ visible: opacity > 0.0
+ spacing: Theme.spacingSmall
+
+ Behavior on opacity {
+ NumberAnimation {
+ duration: Settings.reducedMotion ? 0 : Theme.durationFast
+ }
+ }
+
+ Text {
+ Layout.alignment: Qt.AlignVCenter
+ text: root.isPlaying ? "▶" : "⏸"
+ color: root.isPlaying ? Theme.acidGreen : Theme.textMuted
+ font.family: Theme.fontFamilyMonospace
+ font.pixelSize: Theme.fontSizeCaption
+ font.weight: Theme.fontWeightBold
+ }
+
+ Text {
+ Layout.alignment: Qt.AlignVCenter
+ Layout.fillWidth: true
+ text: root.mediaArtist !== "" ? (root.mediaTitle + " // " + root.mediaArtist) : root.mediaTitle
+ color: Theme.textPrimary
+ font.family: Theme.fontFamilyMonospace
+ font.pixelSize: Theme.fontSizeCaption
+ font.weight: Theme.fontWeightNormal
+ elide: Text.ElideRight
+ maximumLineCount: 1
+ }
+
+ // Animated Equalizer Bars
+ RowLayout {
+ Layout.alignment: Qt.AlignVCenter
+ Layout.preferredHeight: 14
+ spacing: 2
+
+ Repeater {
+ model: 4
+
+ Rectangle {
+ id: eqBar
+ required property int index
+ Layout.alignment: Qt.AlignBottom
+ Layout.preferredWidth: 3
+ Layout.preferredHeight: root.isPlaying
+ ? Math.max(3, Math.round((Math.sin((eqBar.index * 1.1) + root.visualizerPhase) * 0.4 + 0.5) * 14))
+ : 3
+ color: root.isPlaying ? Theme.acidGreen : Theme.gray600
+ radius: 1
+ }
+ }
+ }
+ }
+
// =========================================================================
// Expanded View (Active Notification Banner)
// =========================================================================
@@ -289,10 +454,15 @@ Rectangle {
} else {
collapseTimer.stop();
root.isExpanded = false;
- root.state = "compact";
+ root.state = root.hasMedia ? "media" : "compact";
OverlayController.openEventLog();
}
}
}
+
+ onWheel: (wheel) => {
+ const delta = wheel.angleDelta.y > 0 ? 0.05 : -0.05;
+ AudioService.stepVolume(delta);
+ }
}
}
From 462b938de0b90a36f9a57d759e0ef6c44cb284a7 Mon Sep 17 00:00:00 2001
From: reze-dev <25588579+reze-dev@users.noreply.github.com>
Date: Tue, 29 Sep 2026 02:00:24 +0530
Subject: [PATCH 46/48] chore(flake.lock): bump flake.lock inputs
---
flake.lock | 44 ++++++++++++++++++++++----------------------
1 file changed, 22 insertions(+), 22 deletions(-)
diff --git a/flake.lock b/flake.lock
index 8fd68aae..d2377da3 100644
--- a/flake.lock
+++ b/flake.lock
@@ -250,11 +250,11 @@
]
},
"locked": {
- "lastModified": 1790292277,
- "narHash": "sha256-z+knvdMItRrMonmG4RIsqKYIa/hwYUzKjotDTfC5DDc=",
+ "lastModified": 1790365885,
+ "narHash": "sha256-iQ0ebhiVo64NktTnwft9hNxlFu4j5cvljVxyJaEVIP4=",
"owner": "nix-community",
"repo": "home-manager",
- "rev": "4b9add8645d5e2b0f7de18f7a442d08fe2bdcc91",
+ "rev": "7b4c5ec4bedaf1e062bbc1bcaeddbc6bd242aa1b",
"type": "github"
},
"original": {
@@ -339,11 +339,11 @@
"xdph": "xdph"
},
"locked": {
- "lastModified": 1790173459,
- "narHash": "sha256-GR2NqjxK1C+Z2JxuwmpyaSvTwBCuKFqiznuQKRfSLYQ=",
+ "lastModified": 1790542952,
+ "narHash": "sha256-/ogvT9EYl8gwD3UeBWfVOLNCIFFlvvVn8AaM9Lf052I=",
"ref": "refs/heads/main",
- "rev": "e368c13c27a42a173b9e08fa0bf413f9f7073187",
- "revCount": 7828,
+ "rev": "4bb6844b0351e4fbf2e3d4e46ae71b551a0e0a42",
+ "revCount": 7834,
"submodules": true,
"type": "git",
"url": "https://github.com/hyprwm/Hyprland"
@@ -688,11 +688,11 @@
]
},
"locked": {
- "lastModified": 1789892076,
- "narHash": "sha256-Dkg4VKPmDPqTwaiw2WH5br73tXoL1qrOO0xJnR4TlcA=",
+ "lastModified": 1790499302,
+ "narHash": "sha256-Gbf/U9ptJhQh0qnDnJ/+a8MAcq1HXunbArUTHR2wNYk=",
"owner": "nix-community",
"repo": "nix-index-database",
- "rev": "9ad722673ab3b3f91f02135e53775825b240b869",
+ "rev": "161d7c91accd93034bb3c295224d9d895a778573",
"type": "github"
},
"original": {
@@ -765,11 +765,11 @@
},
"nixpkgs_3": {
"locked": {
- "lastModified": 1790185690,
- "narHash": "sha256-xJ+X4hBtOcAFGBOe5nAMyMUeF9foJBmIOu3NjBqBycU=",
+ "lastModified": 1790578696,
+ "narHash": "sha256-ZoxIApko70jCdbH3l20HWXOBaT2HZd87orzd2yJ9dVE=",
"owner": "NixOS",
"repo": "nixpkgs",
- "rev": "4975466d324710c576dc11ad614684e6bd8cad8e",
+ "rev": "7a0f122f5090cf4c2ade2a13a0e229d4e19ba71f",
"type": "github"
},
"original": {
@@ -847,11 +847,11 @@
]
},
"locked": {
- "lastModified": 1790247521,
- "narHash": "sha256-TNfgoHsqsYYvaJImlWctfRkh4PseTagzegU1Dgdbehw=",
+ "lastModified": 1790498116,
+ "narHash": "sha256-rs9meAYxW3zzrh43yaW7htrqCD+X9+pupDPHN86fumI=",
"owner": "Mic92",
"repo": "sops-nix",
- "rev": "2bd00bd9bb35fe6d114888c8f1c2e946c541dd8f",
+ "rev": "5efb5a6f4f5ab192817d28557dd4d650fa14d866",
"type": "github"
},
"original": {
@@ -882,11 +882,11 @@
]
},
"locked": {
- "lastModified": 1788846942,
- "narHash": "sha256-2nBOxV/56R/z1lAN6QunoV/w3kEQ/CJXf2lpMT6n5Zw=",
+ "lastModified": 1790472687,
+ "narHash": "sha256-6EylFN8ZrzmQQTpOGrKChlAKR1fV9G2DCBiXA8eOGFc=",
"owner": "fabioluciano",
"repo": "tmux-powerkit",
- "rev": "6d7f8ff018aff96726eadaad29c90664f5bd1f3c",
+ "rev": "e9ee43ffdc8971291aee5a62fca630ee242372be",
"type": "github"
},
"original": {
@@ -979,11 +979,11 @@
]
},
"locked": {
- "lastModified": 1790234305,
- "narHash": "sha256-zgshjjI5uhmRQor+bmXH4EYx/E0LUdJEqv0L8WnTFOw=",
+ "lastModified": 1790568199,
+ "narHash": "sha256-h3AHjsOZr9iBEkNfK+Zh7/DoN5iMRpJd/6h/5NxCz9I=",
"owner": "0xc000022070",
"repo": "zen-browser-flake",
- "rev": "2b7fec6557495d97736f1b31904f79312671ca2a",
+ "rev": "e50ed94ebf28bae95397e482dbb1c020f50c20c1",
"type": "github"
},
"original": {
From 27b737f55c7fc764fe98321aca0a773fadcec37f Mon Sep 17 00:00:00 2001
From: reze-dev <25588579+reze-dev@users.noreply.github.com>
Date: Tue, 29 Sep 2026 23:04:36 +0530
Subject: [PATCH 47/48] fix(ui): decouple inner deadzone and bind to
innerRadius in radial settings
---
shell/desktop/surfaces/radial/NavigationController.qml | 2 +-
shell/desktop/surfaces/radial/RadialSettings.qml | 3 ++-
2 files changed, 3 insertions(+), 2 deletions(-)
diff --git a/shell/desktop/surfaces/radial/NavigationController.qml b/shell/desktop/surfaces/radial/NavigationController.qml
index 5ac169b1..b2bb8553 100644
--- a/shell/desktop/surfaces/radial/NavigationController.qml
+++ b/shell/desktop/surfaces/radial/NavigationController.qml
@@ -21,7 +21,7 @@ Item {
property var activeNodes: []
property int categoryCount: 9
- property real innerDeadZone: 160.0
+ property real innerDeadZone: 130.0
property real outerMaxRadius: 420.0
// Signals
diff --git a/shell/desktop/surfaces/radial/RadialSettings.qml b/shell/desktop/surfaces/radial/RadialSettings.qml
index b09a54b0..4c02de92 100644
--- a/shell/desktop/surfaces/radial/RadialSettings.qml
+++ b/shell/desktop/surfaces/radial/RadialSettings.qml
@@ -337,6 +337,7 @@ FocusScope {
surfaceHeight: root.height
activeNodes: skillTree.activeNodes
categoryCount: settingsModel.categoryCount
+ innerDeadZone: wheelMenu.innerRadius
onCategoryChanged: function(idx) {
root.focusedCategoryIndex = idx;
@@ -394,7 +395,7 @@ FocusScope {
root.focusedCategoryIndex = clickedCat;
}
} else {
- if (distFromCenter < 170) {
+ if (distFromCenter < wheelMenu.innerRadius) {
// Clicked center hub -> expand
root.isExpanded = true;
} else if (distFromCenter > 280) {
From 469e05771462034f034326b6c6d93ef23daf8007 Mon Sep 17 00:00:00 2001
From: reze-dev <25588579+reze-dev@users.noreply.github.com>
Date: Wed, 30 Sep 2026 00:21:46 +0530
Subject: [PATCH 48/48] style(ui): reduce radial branch length and add
checkered background pattern
---
.../surfaces/radial/RadialSettings.qml | 24 +++++++++++++------
shell/desktop/surfaces/radial/SkillTree.qml | 4 ++--
2 files changed, 19 insertions(+), 9 deletions(-)
diff --git a/shell/desktop/surfaces/radial/RadialSettings.qml b/shell/desktop/surfaces/radial/RadialSettings.qml
index 4c02de92..6d5788a5 100644
--- a/shell/desktop/surfaces/radial/RadialSettings.qml
+++ b/shell/desktop/surfaces/radial/RadialSettings.qml
@@ -151,20 +151,30 @@ FocusScope {
onPaint: {
var ctx = getContext("2d");
ctx.clearRect(0, 0, width, height);
+
+ var step = 48;
+ ctx.fillStyle = Qt.rgba(0, 0, 0, 0.15);
+ for (var y = 0; y < height; y += step) {
+ for (var x = 0; x < width; x += step) {
+ if (((x / step) + (y / step)) % 2 === 0) {
+ ctx.fillRect(x, y, step, step);
+ }
+ }
+ }
+
ctx.strokeStyle = Theme.gray700;
ctx.lineWidth = 0.5;
- var step = 48;
- for (var x = step; x < width; x += step) {
+ for (var x2 = step; x2 < width; x2 += step) {
ctx.beginPath();
- ctx.moveTo(x, 0);
- ctx.lineTo(x, height);
+ ctx.moveTo(x2, 0);
+ ctx.lineTo(x2, height);
ctx.stroke();
}
- for (var y = step; y < height; y += step) {
+ for (var y2 = step; y2 < height; y2 += step) {
ctx.beginPath();
- ctx.moveTo(0, y);
- ctx.lineTo(width, y);
+ ctx.moveTo(0, y2);
+ ctx.lineTo(width, y2);
ctx.stroke();
}
}
diff --git a/shell/desktop/surfaces/radial/SkillTree.qml b/shell/desktop/surfaces/radial/SkillTree.qml
index 748a63ce..31380161 100644
--- a/shell/desktop/surfaces/radial/SkillTree.qml
+++ b/shell/desktop/surfaces/radial/SkillTree.qml
@@ -116,7 +116,7 @@ Item {
y1: root.wheelCenterY
x2: (root.wheelCenterX + previewSubtree.r1Point.x)
y2: (root.wheelCenterY + previewSubtree.r1Point.y)
- node1Radius: root.outerRadius + 5
+ node1Radius: root.outerRadius + 10
node2Radius: 15
isActive: previewSubtree.isCatFocused
isPreview: true
@@ -274,7 +274,7 @@ Item {
y1: root.wheelCenterY
x2: root.branchOriginX + (root.rootNode ? root.rootNode.pos.x : 70) + cascadeController.offset0
y2: root.wheelCenterY
- node1Radius: root.outerRadius + 5
+ node1Radius: root.outerRadius + 10
node2Radius: 16
isActive: true
isPreview: false