gini-toolkit 6.0.1.dev0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- gini/__init__.py +12 -0
- gini/__main__.py +107 -0
- gini/_version.py +24 -0
- gini/agent/__init__.py +17 -0
- gini/agent/agent_gamemaster.py +140 -0
- gini/agent/api.py +291 -0
- gini/agent/ask.py +123 -0
- gini/agent/authoring.py +72 -0
- gini/agent/blackboard.py +114 -0
- gini/agent/contracts.py +142 -0
- gini/agent/domains.py +91 -0
- gini/agent/embed.py +123 -0
- gini/agent/gamemaster.py +256 -0
- gini/agent/kb.py +148 -0
- gini/agent/lesson_resolver.py +261 -0
- gini/agent/llm/__init__.py +5 -0
- gini/agent/llm/backend.py +43 -0
- gini/agent/llm/fake.py +25 -0
- gini/agent/llm/ollama.py +206 -0
- gini/agent/loop.py +258 -0
- gini/agent/mcp_server.py +86 -0
- gini/agent/meaning.py +225 -0
- gini/agent/mission.py +210 -0
- gini/agent/mission_controller.py +208 -0
- gini/agent/narration.py +116 -0
- gini/agent/notifier.py +86 -0
- gini/agent/personas.py +79 -0
- gini/agent/reasoning.py +172 -0
- gini/agent/recall.py +248 -0
- gini/agent/session.py +79 -0
- gini/agent/teaching_center.py +482 -0
- gini/agent/tools/__init__.py +3 -0
- gini/agent/tools/registry.py +193 -0
- gini/agent/twin/__init__.py +28 -0
- gini/agent/twin/authoring.py +71 -0
- gini/agent/twin/contracts.py +54 -0
- gini/agent/twin/dialectic.py +189 -0
- gini/agent/twin/harness.py +93 -0
- gini/agent/twin/justify.py +156 -0
- gini/agent/twin/learner.py +64 -0
- gini/agent/twin/mission.py +60 -0
- gini/agent/twin/os_coach.py +79 -0
- gini/agent/twin/salience.py +30 -0
- gini/agent/understand.py +250 -0
- gini/agent/verifiers.py +106 -0
- gini/agent/wizard.py +178 -0
- gini/agent/xv6_pack.py +74 -0
- gini/app/__init__.py +3 -0
- gini/app/context.py +368 -0
- gini/app/paths.py +121 -0
- gini/data/README.md +21 -0
- gini/domain/__init__.py +9 -0
- gini/domain/assembly.py +209 -0
- gini/domain/authoring.py +353 -0
- gini/domain/blueprints.py +5 -0
- gini/domain/capabilities.py +177 -0
- gini/domain/catalog.py +85 -0
- gini/domain/certify.py +201 -0
- gini/domain/compose.py +413 -0
- gini/domain/composition.py +88 -0
- gini/domain/concepts.py +383 -0
- gini/domain/connection_rules.py +269 -0
- gini/domain/constraints.py +153 -0
- gini/domain/content.py +59 -0
- gini/domain/cpu_journey.py +89 -0
- gini/domain/devices.py +747 -0
- gini/domain/diagnose.py +201 -0
- gini/domain/element_guide.py +327 -0
- gini/domain/explain.py +90 -0
- gini/domain/fingerprint.py +201 -0
- gini/domain/firewall.py +34 -0
- gini/domain/flowlog.py +61 -0
- gini/domain/flowtable.py +179 -0
- gini/domain/fragment_yaml.py +230 -0
- gini/domain/fragments.py +169 -0
- gini/domain/games/__init__.py +2 -0
- gini/domain/games/paging_games.py +119 -0
- gini/domain/games/policy_game.py +86 -0
- gini/domain/games/process_game.py +48 -0
- gini/domain/games/thrash_game.py +75 -0
- gini/domain/games/translate_game.py +60 -0
- gini/domain/games/trap_game.py +86 -0
- gini/domain/grader.py +155 -0
- gini/domain/grouping.py +67 -0
- gini/domain/legality.py +103 -0
- gini/domain/lesson.py +241 -0
- gini/domain/lexicon.py +150 -0
- gini/domain/machine_state.py +410 -0
- gini/domain/missions/networking/basic-lan.yaml +32 -0
- gini/domain/missions/networking/cache-in-front.yaml +23 -0
- gini/domain/missions/networking/decouple-with-queue.yaml +31 -0
- gini/domain/missions/networking/drive-load.yaml +20 -0
- gini/domain/missions/networking/fix-the-address.yaml +75 -0
- gini/domain/missions/networking/fix-the-lan.yaml +43 -0
- gini/domain/missions/networking/inspect-flows.yaml +16 -0
- gini/domain/missions/networking/k8s-autoscale.yaml +27 -0
- gini/domain/missions/networking/least-privilege.yaml +21 -0
- gini/domain/missions/networking/load-balanced-web.yaml +29 -0
- gini/domain/missions/networking/observe-it.yaml +24 -0
- gini/domain/missions/networking/put-in-vpc.yaml +30 -0
- gini/domain/missions/networking/reachability-boundary.yaml +56 -0
- gini/domain/missions/networking/sdn-reactive.yaml +35 -0
- gini/domain/missions/networking/send-request.yaml +19 -0
- gini/domain/missions/networking/serverless-api.yaml +25 -0
- gini/domain/missions/networking/service-chain.yaml +33 -0
- gini/domain/missions/os/lottery-fix.yaml +19 -0
- gini/domain/missions/os/priority-fix.yaml +24 -0
- gini/domain/missions.py +111 -0
- gini/domain/modulechain.py +36 -0
- gini/domain/objectives.py +488 -0
- gini/domain/os_zoo.py +79 -0
- gini/domain/paging_sim.py +141 -0
- gini/domain/pricing.py +199 -0
- gini/domain/probes.py +226 -0
- gini/domain/profile.py +142 -0
- gini/domain/recipes.py +738 -0
- gini/domain/riders.py +309 -0
- gini/domain/router_modules.py +224 -0
- gini/domain/routetable.py +67 -0
- gini/domain/scoring.py +76 -0
- gini/domain/staging.py +122 -0
- gini/domain/syscall_builder.py +144 -0
- gini/domain/topic_cloud.py +62 -0
- gini/domain/topology.py +213 -0
- gini/domain/vocabulary.py +51 -0
- gini/domain/xv6.py +808 -0
- gini/domain/xv6_fs.py +250 -0
- gini/domain/xv6_runner.py +113 -0
- gini/domain/xv6_vm.py +385 -0
- gini/gloader.py +17 -0
- gini/runtime/__init__.py +18 -0
- gini/runtime/cloudfabric_agent.py +370 -0
- gini/runtime/console.py +68 -0
- gini/runtime/control.py +70 -0
- gini/runtime/frame.py +138 -0
- gini/runtime/gbridge.py +638 -0
- gini/runtime/grouter.py +223 -0
- gini/runtime/hostsim.py +90 -0
- gini/runtime/shuttle.py +348 -0
- gini/runtime/switch.py +109 -0
- gini/runtime/transport.py +77 -0
- gini/runtime/xv6_bridge.py +312 -0
- gini/server/__init__.py +22 -0
- gini/server/__main__.py +74 -0
- gini/server/app.py +140 -0
- gini/server/auth.py +82 -0
- gini/server/policy.py +57 -0
- gini/server/session.py +23 -0
- gini/services/__init__.py +15 -0
- gini/services/boardflash.py +248 -0
- gini/services/boardsetup.py +374 -0
- gini/services/cloud_catalog.py +143 -0
- gini/services/compiler.py +1858 -0
- gini/services/discovery.py +324 -0
- gini/services/gloader.py +183 -0
- gini/services/orchestrator.py +1460 -0
- gini/services/persistence.py +28 -0
- gini/services/probe_runner.py +149 -0
- gini/services/project.py +217 -0
- gini/services/remote.py +93 -0
- gini/services/rider_runner.py +96 -0
- gini/services/rider_session.py +171 -0
- gini/services/shadow_store.py +52 -0
- gini/services/terminal.py +45 -0
- gini/setup/__init__.py +17 -0
- gini/setup/cli.py +109 -0
- gini/setup/images.py +33 -0
- gini/setup/marker.py +43 -0
- gini/setup/runtime.py +69 -0
- gini/ui/__init__.py +3 -0
- gini/ui/assets/app_icon.icns +0 -0
- gini/ui/assets/app_icon.ico +0 -0
- gini/ui/assets/app_icon.png +0 -0
- gini/ui/assets/app_icon_1024.png +0 -0
- gini/ui/assets/cue/_w.txt +1 -0
- gini/ui/assets/cue/ai.png +0 -0
- gini/ui/assets/cue/canvas.png +0 -0
- gini/ui/assets/cue/cloud.png +0 -0
- gini/ui/assets/cue/cost.png +0 -0
- gini/ui/assets/cue/dark/ai.png +0 -0
- gini/ui/assets/cue/dark/canvas.png +0 -0
- gini/ui/assets/cue/dark/cloud.png +0 -0
- gini/ui/assets/cue/dark/cost.png +0 -0
- gini/ui/assets/cue/dark/metrics.png +0 -0
- gini/ui/assets/cue/dark/router.png +0 -0
- gini/ui/assets/cue/dark/run.png +0 -0
- gini/ui/assets/cue/dark/serverless.png +0 -0
- gini/ui/assets/cue/dark/settings.png +0 -0
- gini/ui/assets/cue/dark/welcome.png +0 -0
- gini/ui/assets/cue/dark/wizard.png +0 -0
- gini/ui/assets/cue/ginibrand/ai.png +0 -0
- gini/ui/assets/cue/ginibrand/canvas.png +0 -0
- gini/ui/assets/cue/ginibrand/cloud.png +0 -0
- gini/ui/assets/cue/ginibrand/cost.png +0 -0
- gini/ui/assets/cue/ginibrand/metrics.png +0 -0
- gini/ui/assets/cue/ginibrand/router.png +0 -0
- gini/ui/assets/cue/ginibrand/run.png +0 -0
- gini/ui/assets/cue/ginibrand/serverless.png +0 -0
- gini/ui/assets/cue/ginibrand/settings.png +0 -0
- gini/ui/assets/cue/ginibrand/welcome.png +0 -0
- gini/ui/assets/cue/ginibrand/wizard.png +0 -0
- gini/ui/assets/cue/highcontrast/ai.png +0 -0
- gini/ui/assets/cue/highcontrast/canvas.png +0 -0
- gini/ui/assets/cue/highcontrast/cloud.png +0 -0
- gini/ui/assets/cue/highcontrast/cost.png +0 -0
- gini/ui/assets/cue/highcontrast/metrics.png +0 -0
- gini/ui/assets/cue/highcontrast/router.png +0 -0
- gini/ui/assets/cue/highcontrast/run.png +0 -0
- gini/ui/assets/cue/highcontrast/serverless.png +0 -0
- gini/ui/assets/cue/highcontrast/settings.png +0 -0
- gini/ui/assets/cue/highcontrast/welcome.png +0 -0
- gini/ui/assets/cue/highcontrast/wizard.png +0 -0
- gini/ui/assets/cue/light/ai.png +0 -0
- gini/ui/assets/cue/light/canvas.png +0 -0
- gini/ui/assets/cue/light/cloud.png +0 -0
- gini/ui/assets/cue/light/cost.png +0 -0
- gini/ui/assets/cue/light/metrics.png +0 -0
- gini/ui/assets/cue/light/router.png +0 -0
- gini/ui/assets/cue/light/run.png +0 -0
- gini/ui/assets/cue/light/serverless.png +0 -0
- gini/ui/assets/cue/light/settings.png +0 -0
- gini/ui/assets/cue/light/welcome.png +0 -0
- gini/ui/assets/cue/light/wizard.png +0 -0
- gini/ui/assets/cue/metrics.png +0 -0
- gini/ui/assets/cue/router.png +0 -0
- gini/ui/assets/cue/run.png +0 -0
- gini/ui/assets/cue/serverless.png +0 -0
- gini/ui/assets/cue/settings.png +0 -0
- gini/ui/assets/cue/welcome.png +0 -0
- gini/ui/assets/cue/wizard.png +0 -0
- gini/ui/assistant.py +2111 -0
- gini/ui/author_dialog.py +184 -0
- gini/ui/board_dialog.py +247 -0
- gini/ui/branding.py +21 -0
- gini/ui/canvas.py +2007 -0
- gini/ui/chat_panel.py +7 -0
- gini/ui/cpu_journey.py +212 -0
- gini/ui/cpu_lab.py +306 -0
- gini/ui/cue_cards.py +214 -0
- gini/ui/dashboard.py +222 -0
- gini/ui/diagnose_game.py +336 -0
- gini/ui/fingerprint_lab.py +219 -0
- gini/ui/flash_dialog.py +244 -0
- gini/ui/flow_layout.py +63 -0
- gini/ui/fragment_manager.py +1415 -0
- gini/ui/game_catalog.py +184 -0
- gini/ui/game_renderers.py +340 -0
- gini/ui/games_lab.py +90 -0
- gini/ui/inspector.py +1055 -0
- gini/ui/live_metrics.py +130 -0
- gini/ui/machine_lab.py +1412 -0
- gini/ui/main_window.py +3153 -0
- gini/ui/memory_lab.py +371 -0
- gini/ui/mission_panel.py +302 -0
- gini/ui/mode_indicator.py +227 -0
- gini/ui/palette.py +112 -0
- gini/ui/peripherals.py +218 -0
- gini/ui/process_tree.py +130 -0
- gini/ui/reset_dialog.py +179 -0
- gini/ui/router_lab.py +776 -0
- gini/ui/run_button.py +183 -0
- gini/ui/settings_dialog.py +234 -0
- gini/ui/signin_dialog.py +111 -0
- gini/ui/storage_lab.py +219 -0
- gini/ui/syscall_builder.py +235 -0
- gini/ui/syscall_lab.py +152 -0
- gini/ui/theme/__init__.py +5 -0
- gini/ui/theme/icons.py +145 -0
- gini/ui/theme/manager.py +291 -0
- gini/ui/theme/tokens.py +194 -0
- gini/ui/trap_lab.py +270 -0
- gini/ui/worker_host.py +102 -0
- gini/ui/zoo_lab.py +112 -0
- gini_toolkit-6.0.1.dev0.dist-info/METADATA +77 -0
- gini_toolkit-6.0.1.dev0.dist-info/RECORD +278 -0
- gini_toolkit-6.0.1.dev0.dist-info/WHEEL +5 -0
- gini_toolkit-6.0.1.dev0.dist-info/entry_points.txt +3 -0
- gini_toolkit-6.0.1.dev0.dist-info/top_level.txt +1 -0
gini/ui/theme/icons.py
ADDED
|
@@ -0,0 +1,145 @@
|
|
|
1
|
+
"""License-safe flat line-icon set, recolored per theme/category and rendered to QIcon.
|
|
2
|
+
|
|
3
|
+
Icons are hand-authored single-color SVG line glyphs (our own work, no third-party
|
|
4
|
+
licensing) drawn on a 24x24 grid. They take their color from the active theme, so the
|
|
5
|
+
same glyph reads correctly on dark, light, and brand themes. Rendering goes through
|
|
6
|
+
QSvgRenderer at the target device-pixel ratio for crisp output on HiDPI displays.
|
|
7
|
+
"""
|
|
8
|
+
from __future__ import annotations
|
|
9
|
+
|
|
10
|
+
from PySide6.QtCore import QByteArray, QRectF, Qt
|
|
11
|
+
from PySide6.QtGui import QColor, QIcon, QPainter, QPixmap
|
|
12
|
+
|
|
13
|
+
try:
|
|
14
|
+
from PySide6.QtSvg import QSvgRenderer
|
|
15
|
+
_HAVE_SVG = True
|
|
16
|
+
except Exception: # pragma: no cover
|
|
17
|
+
_HAVE_SVG = False
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
# inner SVG body for each glyph (stroke-based unless listed in _FILLED)
|
|
21
|
+
_BODY: dict[str, str] = {
|
|
22
|
+
# --- networking ---
|
|
23
|
+
"router": '<rect x="2.5" y="9" width="19" height="9" rx="2"/><path d="M6.5 9V6M12 9V6M17.5 9V6"/><path d="M6.5 13.5h.01M10 13.5h.01M13.5 13.5h.01"/>',
|
|
24
|
+
"switch": '<rect x="2.5" y="8" width="19" height="8" rx="2"/><path d="M6 16v2.5M10 16v2.5M14 16v2.5M18 16v2.5M6 8V5.5M18 8V5.5"/>',
|
|
25
|
+
"hub": '<circle cx="12" cy="12" r="3"/><path d="M12 3v4M12 17v4M3 12h4M17 12h4M5.6 5.6l2.8 2.8M15.6 15.6l2.8 2.8M18.4 5.6l-2.8 2.8M8.4 15.6l-2.8 2.8"/>',
|
|
26
|
+
"host": '<rect x="3" y="4" width="18" height="12" rx="2"/><path d="M8 20h8M12 16v4"/>',
|
|
27
|
+
"firewall": '<rect x="3" y="4" width="18" height="16" rx="2"/><path d="M3 9.3h18M3 14.6h18M9 4v5.3M15 9.3v5.3M9 14.6V20"/>',
|
|
28
|
+
"wifi": '<path d="M5 12.5a10 10 0 0 1 14 0"/><path d="M8 15.5a6 6 0 0 1 8 0"/><path d="M11 18.4a1.6 1.6 0 0 1 2 0"/>',
|
|
29
|
+
"cloud": '<path d="M7 18a4 4 0 0 1 0-8 5 5 0 0 1 9.6-1.4A3.5 3.5 0 0 1 18 18z"/>',
|
|
30
|
+
# GINI32: a small board with a radiating antenna — a real chip, not a cloud
|
|
31
|
+
"gini32": '<rect x="3" y="10" width="11" height="9" rx="1.5"/>'
|
|
32
|
+
'<path d="M6 19v2M11 19v2M6 10V8.5M11 10V8.5"/>'
|
|
33
|
+
'<path d="M17 9.5a4 4 0 0 1 0 5M19.5 7a7 7 0 0 1 0 10"/>',
|
|
34
|
+
# --- sdn ---
|
|
35
|
+
"ovs": '<rect x="2.5" y="9" width="19" height="7" rx="2"/><path d="M12 3v6"/><path d="M7 20l5-3 5 3"/>',
|
|
36
|
+
"controller": '<rect x="4" y="4" width="16" height="16" rx="3"/><circle cx="12" cy="11" r="2.5"/><path d="M12 13.5V17M9.5 11H7M17 11h-2.5"/>',
|
|
37
|
+
# --- containers & k8s ---
|
|
38
|
+
"container": '<path d="M3 7.5 12 3l9 4.5v9L12 21l-9-4.5z"/><path d="M3 7.5 12 12l9-4.5M12 12v9"/>',
|
|
39
|
+
"pod": '<path d="M12 2.5l8.2 4.7v9.6L12 21.5 3.8 16.8V7.2z"/><circle cx="12" cy="12" r="3"/>',
|
|
40
|
+
"k8s_node": '<rect x="3" y="6" width="18" height="12" rx="2"/><circle cx="12" cy="12" r="2.5"/><path d="M7 4v2M17 4v2M7 18v2M17 18v2"/>',
|
|
41
|
+
"k8s_cluster": '<path d="M12 2.5l8.2 4.7v9.6L12 21.5 3.8 16.8V7.2z"/><path d="M12 8v8M8.5 10v4M15.5 10v4"/>',
|
|
42
|
+
"registry": '<rect x="3" y="9" width="18" height="11" rx="2"/><path d="M7 9V5h10v4"/><path d="M8 14h8"/>',
|
|
43
|
+
# --- cloud networking ---
|
|
44
|
+
"vpc": '<rect x="2.5" y="4" width="19" height="16" rx="3" stroke-dasharray="4 3"/><path d="M7 12h10M12 8v8"/>',
|
|
45
|
+
"cloud_subnet": '<rect x="3.5" y="6" width="17" height="12" rx="2" stroke-dasharray="3 3"/><path d="M8 12h8"/>',
|
|
46
|
+
"security_group": '<path d="M12 3l7 3v5c0 4.5-3 7.5-7 9-4-1.5-7-4.5-7-9V6z"/><path d="M9.3 12l1.8 1.8L15 10"/>',
|
|
47
|
+
"gateway": '<path d="M3 12h18"/><path d="M8 7l-4 5 4 5M16 7l4 5-4 5"/>',
|
|
48
|
+
"load_balancer": '<circle cx="12" cy="5" r="2.3"/><circle cx="5" cy="19" r="2.3"/><circle cx="12" cy="19" r="2.3"/><circle cx="19" cy="19" r="2.3"/><path d="M12 7.3v3M12 10.3l-7 6.4M12 10.3v6.4M12 10.3l7 6.4"/>',
|
|
49
|
+
# --- compute ---
|
|
50
|
+
"instance": '<rect x="6" y="6" width="12" height="12" rx="2"/><path d="M9 3v3M12 3v3M15 3v3M9 18v3M12 18v3M15 18v3M3 9h3M3 12h3M3 15h3M18 9h3M18 12h3M18 15h3"/>',
|
|
51
|
+
"instance_group": '<rect x="7" y="3.5" width="13" height="13" rx="2"/><path d="M4 7.5v13h13"/>',
|
|
52
|
+
"region": '<circle cx="12" cy="12" r="9"/><path d="M3 12h18M12 3c2.6 2.4 4 5.6 4 9s-1.4 6.6-4 9c-2.6-2.4-4-5.6-4-9s1.4-6.6 4-9z"/>',
|
|
53
|
+
# --- storage ---
|
|
54
|
+
"object_store": '<path d="M5 7h14l-1.4 12.2a1 1 0 0 1-1 .9H7.4a1 1 0 0 1-1-.9z"/><path d="M3.5 7h17"/>',
|
|
55
|
+
"block_volume": '<rect x="4" y="6" width="16" height="12" rx="2"/><path d="M8 6v12M4 12h4"/>',
|
|
56
|
+
"database": '<ellipse cx="12" cy="6" rx="7" ry="3"/><path d="M5 6v12c0 1.7 3.1 3 7 3s7-1.3 7-3V6"/><path d="M5 12c0 1.7 3.1 3 7 3s7-1.3 7-3"/>',
|
|
57
|
+
# --- serverless ---
|
|
58
|
+
"function": '<path d="M6 20l6-8M18 20 9 4H6"/>',
|
|
59
|
+
"api_gateway": '<rect x="3" y="6" width="18" height="12" rx="2"/><path d="M9 9l-2 3 2 3M15 9l2 3-2 3"/>',
|
|
60
|
+
"queue": '<rect x="3" y="7.5" width="4" height="9" rx="1"/><rect x="10" y="7.5" width="4" height="9" rx="1"/><rect x="17" y="7.5" width="4" height="9" rx="1"/>',
|
|
61
|
+
# --- edge & traffic ---
|
|
62
|
+
"proxy": '<path d="M3 12h7M21 12h-7"/><rect x="9" y="8" width="6" height="8" rx="1.5"/><path d="M5 9l-2 3 2 3M19 9l2 3-2 3"/>',
|
|
63
|
+
"web_app": '<rect x="3" y="5" width="18" height="14" rx="2"/><path d="M3 9h18M6 7h.01M8.5 7h.01"/>',
|
|
64
|
+
# --- streaming & messaging ---
|
|
65
|
+
"stream": '<path d="M3 8c3 0 3-2 6-2s3 2 6 2 3-2 6-2M3 12c3 0 3-2 6-2s3 2 6 2 3-2 6-2M3 16c3 0 3-2 6-2s3 2 6 2 3-2 6-2"/>',
|
|
66
|
+
"messaging": '<path d="M4 5h16v11H8l-4 3z"/><path d="M8 9h8M8 12h5"/>',
|
|
67
|
+
# --- cache & nosql ---
|
|
68
|
+
"cache": '<rect x="3.5" y="5" width="17" height="14" rx="2"/><path d="M11 8l-2.5 4H12l-2 4"/>',
|
|
69
|
+
"nosql": '<path d="M12 3c4 4 4 14 0 18-4-4-4-14 0-18z"/><path d="M5.5 8.5c4 2 9 2 13 0M5.5 15.5c4-2 9-2 13 0"/>',
|
|
70
|
+
# --- observability ---
|
|
71
|
+
"metrics": '<path d="M3 20V4M3 20h18"/><path d="M7 16v-3M11.5 16V9M16 16v-5M20.5 16V7"/>',
|
|
72
|
+
"dashboard": '<rect x="3" y="4" width="18" height="16" rx="2"/><path d="M7 15l3-4 3 2 4-6"/>',
|
|
73
|
+
"tracing": '<circle cx="5" cy="7" r="2"/><circle cx="12" cy="17" r="2"/><circle cx="19" cy="9" r="2"/><path d="M6.6 8.4l4 7M13.6 15.8l4-5.4"/>',
|
|
74
|
+
# --- workload & testing ---
|
|
75
|
+
"load_generator": '<path d="M12 13a8 8 0 0 1 8-8M12 13l5-5"/><path d="M4 19a8 8 0 0 1 2.3-12.7M20 19a8 8 0 0 0-2.3-12.7" stroke-dasharray="2 2.5"/>',
|
|
76
|
+
# --- UI glyphs ---
|
|
77
|
+
"new": '<path d="M12 5v14M5 12h14"/>',
|
|
78
|
+
"open": '<path d="M3 7h6l2 2h10v9a2 2 0 0 1-2 2H3z"/>',
|
|
79
|
+
"save": '<path d="M5 3h11l3 3v15H5z"/><path d="M8 3v5h7M8 13h8v8H8z"/>',
|
|
80
|
+
"compile": '<path d="M9 6l-5 6 5 6M15 6l5 6-5 6"/>',
|
|
81
|
+
"layout": '<rect x="3" y="3" width="7" height="7" rx="1"/><rect x="14" y="3" width="7" height="7" rx="1"/><rect x="3" y="14" width="7" height="7" rx="1"/><rect x="14" y="14" width="7" height="7" rx="1"/>',
|
|
82
|
+
"search": '<circle cx="11" cy="11" r="7"/><path d="M21 21l-4-4"/>',
|
|
83
|
+
"plus": '<path d="M12 5v14M5 12h14"/>',
|
|
84
|
+
"minus": '<path d="M5 12h14"/>',
|
|
85
|
+
"palette": '<circle cx="12" cy="12" r="9"/><circle cx="8.5" cy="9.5" r="1.1"/><circle cx="12" cy="8" r="1.1"/><circle cx="15.5" cy="9.5" r="1.1"/>',
|
|
86
|
+
"robot": '<rect x="4" y="8" width="16" height="11" rx="3"/><path d="M12 8V4M9 13h.01M15 13h.01M9.5 16h5M2 12v3M22 12v3"/>',
|
|
87
|
+
"send": '<path d="M4 12l16-7-7 16-2-7z"/>',
|
|
88
|
+
"grid": '<rect x="3" y="3" width="18" height="18" rx="2"/><path d="M3 9h18M3 15h18M9 3v18M15 3v18"/>',
|
|
89
|
+
"trash": '<path d="M4 7h16M9 7V4h6v3M6 7l1 13h10l1-13"/>',
|
|
90
|
+
"link": '<path d="M9 15l6-6M8.5 8H6a4 4 0 0 0 0 8h2.5M15.5 16H18a4 4 0 0 0 0-8h-2.5"/>',
|
|
91
|
+
"elbow": '<path d="M5 5v6a3 3 0 0 0 3 3h6a3 3 0 0 1 3 3v2"/><circle cx="5" cy="5" r="1.9"/><circle cx="20" cy="19" r="1.9"/>',
|
|
92
|
+
"pencil": '<path d="M4 20.5h4L19 9.5a2 2 0 0 0 0-2.8l-1.7-1.7a2 2 0 0 0-2.8 0L3.5 16.5z"/><path d="M13.5 6.5l4 4"/>',
|
|
93
|
+
"chevron_right": '<path d="M9 6l6 6-6 6"/>',
|
|
94
|
+
"chevron_down": '<path d="M6 9l6 6 6-6"/>',
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
_FILLED = {"play", "stop", "dot"}
|
|
98
|
+
_BODY["play"] = '<path d="M8 5v14l11-7z"/>'
|
|
99
|
+
_BODY["stop"] = '<rect x="6" y="6" width="12" height="12" rx="2"/>'
|
|
100
|
+
_BODY["dot"] = '<circle cx="12" cy="12" r="6"/>'
|
|
101
|
+
|
|
102
|
+
|
|
103
|
+
def _svg(name: str, color: str, stroke_width: float) -> str:
|
|
104
|
+
body = _BODY.get(name, _BODY["host"])
|
|
105
|
+
if name in _FILLED:
|
|
106
|
+
return (f'<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" '
|
|
107
|
+
f'fill="{color}" stroke="none">{body}</svg>')
|
|
108
|
+
return (f'<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" '
|
|
109
|
+
f'stroke="{color}" stroke-width="{stroke_width}" stroke-linecap="round" '
|
|
110
|
+
f'stroke-linejoin="round">{body}</svg>')
|
|
111
|
+
|
|
112
|
+
|
|
113
|
+
_cache: dict[tuple, QIcon] = {}
|
|
114
|
+
|
|
115
|
+
|
|
116
|
+
def render_pixmap(name: str, color: str, size: int = 22, stroke_width: float = 1.7,
|
|
117
|
+
ratio: float = 2.0) -> QPixmap:
|
|
118
|
+
px = QPixmap(int(size * ratio), int(size * ratio))
|
|
119
|
+
px.setDevicePixelRatio(ratio)
|
|
120
|
+
px.fill(Qt.transparent)
|
|
121
|
+
if _HAVE_SVG:
|
|
122
|
+
renderer = QSvgRenderer(QByteArray(_svg(name, color, stroke_width).encode()))
|
|
123
|
+
p = QPainter(px)
|
|
124
|
+
p.setRenderHint(QPainter.Antialiasing, True)
|
|
125
|
+
renderer.render(p, QRectF(0, 0, size, size))
|
|
126
|
+
p.end()
|
|
127
|
+
else: # pragma: no cover - fallback shape
|
|
128
|
+
p = QPainter(px)
|
|
129
|
+
p.setRenderHint(QPainter.Antialiasing, True)
|
|
130
|
+
p.setBrush(QColor(color))
|
|
131
|
+
p.setPen(Qt.NoPen)
|
|
132
|
+
p.drawRoundedRect(QRectF(3, 3, size - 6, size - 6), 5, 5)
|
|
133
|
+
p.end()
|
|
134
|
+
return px
|
|
135
|
+
|
|
136
|
+
|
|
137
|
+
def icon(name: str, color: str, size: int = 22, stroke_width: float = 1.7) -> QIcon:
|
|
138
|
+
key = (name, color, size, round(stroke_width, 2))
|
|
139
|
+
if key not in _cache:
|
|
140
|
+
_cache[key] = QIcon(render_pixmap(name, color, size, stroke_width))
|
|
141
|
+
return _cache[key]
|
|
142
|
+
|
|
143
|
+
|
|
144
|
+
def clear_cache() -> None:
|
|
145
|
+
_cache.clear()
|
gini/ui/theme/manager.py
ADDED
|
@@ -0,0 +1,291 @@
|
|
|
1
|
+
"""Token -> QSS generator and the ThemeManager that applies it app-wide."""
|
|
2
|
+
from __future__ import annotations
|
|
3
|
+
|
|
4
|
+
from PySide6.QtCore import QObject, Signal
|
|
5
|
+
from PySide6.QtGui import QColor, QPalette
|
|
6
|
+
from PySide6.QtWidgets import QApplication
|
|
7
|
+
|
|
8
|
+
from .tokens import Theme, get_theme
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
def _qc(spec: str) -> QColor:
|
|
12
|
+
if spec.startswith("rgba("):
|
|
13
|
+
n = [int(float(x)) for x in spec[5:-1].split(",")]
|
|
14
|
+
return QColor(n[0], n[1], n[2], n[3] if len(n) > 3 else 255)
|
|
15
|
+
return QColor(spec)
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
_FONT_STACK: str | None = None
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
def _ui_font_stack() -> str:
|
|
22
|
+
"""A font-family list whose FIRST entry is actually installed on this machine.
|
|
23
|
+
|
|
24
|
+
Leading with a present family avoids Qt's slow missing-family alias lookup (the
|
|
25
|
+
'Populating font family aliases … Replace uses of missing font family "Inter"'
|
|
26
|
+
warning + delay seen on macOS, which lacks Inter). Inter is still used when present.
|
|
27
|
+
"""
|
|
28
|
+
global _FONT_STACK
|
|
29
|
+
if _FONT_STACK is not None:
|
|
30
|
+
return _FONT_STACK
|
|
31
|
+
prefs = ["Inter", "SF Pro Text", "Helvetica Neue", "Segoe UI", "Roboto", "Arial"]
|
|
32
|
+
try:
|
|
33
|
+
from PySide6.QtGui import QFontDatabase
|
|
34
|
+
avail = set(QFontDatabase.families())
|
|
35
|
+
present = [f for f in prefs if f in avail]
|
|
36
|
+
except Exception:
|
|
37
|
+
present = []
|
|
38
|
+
present = present[:4] or ["Helvetica Neue"]
|
|
39
|
+
_FONT_STACK = ", ".join(f'"{f}"' for f in present) + ", sans-serif"
|
|
40
|
+
return _FONT_STACK
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
def build_palette(t: Theme) -> QPalette:
|
|
44
|
+
"""A full palette so even unstyled surfaces (scroll viewports, tab panes,
|
|
45
|
+
plain QWidgets) follow the theme instead of falling back to system light."""
|
|
46
|
+
p = QPalette()
|
|
47
|
+
white = QColor("#ffffff")
|
|
48
|
+
p.setColor(QPalette.Window, _qc(t.bg))
|
|
49
|
+
p.setColor(QPalette.WindowText, _qc(t.text))
|
|
50
|
+
p.setColor(QPalette.Base, _qc(t.bg3))
|
|
51
|
+
p.setColor(QPalette.AlternateBase, _qc(t.bg2))
|
|
52
|
+
p.setColor(QPalette.Text, _qc(t.text))
|
|
53
|
+
p.setColor(QPalette.Button, _qc(t.panel2))
|
|
54
|
+
p.setColor(QPalette.ButtonText, _qc(t.text))
|
|
55
|
+
p.setColor(QPalette.ToolTipBase, _qc(t.panel2))
|
|
56
|
+
p.setColor(QPalette.ToolTipText, _qc(t.text))
|
|
57
|
+
p.setColor(QPalette.PlaceholderText, _qc(t.faint))
|
|
58
|
+
p.setColor(QPalette.Highlight, _qc(t.accent))
|
|
59
|
+
p.setColor(QPalette.HighlightedText, white)
|
|
60
|
+
p.setColor(QPalette.BrightText, white)
|
|
61
|
+
p.setColor(QPalette.Link, _qc(t.accent))
|
|
62
|
+
for role in (QPalette.Text, QPalette.WindowText, QPalette.ButtonText):
|
|
63
|
+
p.setColor(QPalette.Disabled, role, _qc(t.faint))
|
|
64
|
+
return p
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
# UI text-size presets (Settings → Appearance). A single scale factor multiplies the base font
|
|
68
|
+
# sizes in the stylesheet, so the whole UI grows/shrinks together.
|
|
69
|
+
FONT_SCALES = {"Normal": 1.0, "Large": 1.16, "Extra Large": 1.32}
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
def scale_for(label: str) -> float:
|
|
73
|
+
return FONT_SCALES.get((label or "Normal").strip(), 1.0)
|
|
74
|
+
|
|
75
|
+
|
|
76
|
+
# The active UI text scale, published module-wide so custom-painted widgets (canvas cards, the
|
|
77
|
+
# dashboard, palette headers, toolbar pills) that set their own point sizes can honour the setting.
|
|
78
|
+
_ACTIVE_SCALE = 1.0
|
|
79
|
+
|
|
80
|
+
|
|
81
|
+
def ui_scale() -> float:
|
|
82
|
+
return _ACTIVE_SCALE
|
|
83
|
+
|
|
84
|
+
|
|
85
|
+
def sp(base_pt: float) -> int:
|
|
86
|
+
"""A point size scaled by the active UI text-size setting (never below 1)."""
|
|
87
|
+
return max(1, round(base_pt * _ACTIVE_SCALE))
|
|
88
|
+
|
|
89
|
+
|
|
90
|
+
import re as _re
|
|
91
|
+
|
|
92
|
+
_FS_RE = _re.compile(r"(font-size:\s*)(\d+)px")
|
|
93
|
+
|
|
94
|
+
|
|
95
|
+
def scale_css(css: str) -> str:
|
|
96
|
+
"""Scale every `font-size: Npx` in a stylesheet by the active UI text size, leaving all other
|
|
97
|
+
px (padding, borders, radii) untouched. Lets widgets that set their own stylesheet honour the
|
|
98
|
+
Settings → Text size choice without hand-editing each size."""
|
|
99
|
+
if abs(_ACTIVE_SCALE - 1.0) < 1e-3:
|
|
100
|
+
return css
|
|
101
|
+
return _FS_RE.sub(lambda m: f"{m.group(1)}{max(1, round(int(m.group(2)) * _ACTIVE_SCALE))}px", css)
|
|
102
|
+
|
|
103
|
+
|
|
104
|
+
def build_qss(t: Theme, scale: float = 1.0) -> str:
|
|
105
|
+
base = round(13 * scale) # the app-wide default size
|
|
106
|
+
small = round(11 * scale) # the smaller section-header size
|
|
107
|
+
return f"""
|
|
108
|
+
* {{
|
|
109
|
+
font-family: {_ui_font_stack()};
|
|
110
|
+
font-size: {base}px;
|
|
111
|
+
color: {t.text};
|
|
112
|
+
outline: none;
|
|
113
|
+
}}
|
|
114
|
+
QMainWindow, QDialog {{ background: {t.bg}; }}
|
|
115
|
+
QWidget#Sidebar, QWidget#Inspector, QWidget#Console {{ background: {t.panel}; }}
|
|
116
|
+
QFrame#Card {{ background: {t.panel2}; border: 1px solid {t.line}; border-radius: 10px; }}
|
|
117
|
+
|
|
118
|
+
QToolBar {{
|
|
119
|
+
background: qlineargradient(x1:0, y1:0, x2:0, y2:1, stop:0 {t.panel}, stop:1 {t.panel2});
|
|
120
|
+
border: none; border-bottom: 1px solid {t.line2};
|
|
121
|
+
spacing: 4px; padding: 7px 12px;
|
|
122
|
+
}}
|
|
123
|
+
QToolBar::separator {{ background: {t.line}; width: 1px; margin: 6px 8px; border-radius: 1px; }}
|
|
124
|
+
|
|
125
|
+
QToolButton, QPushButton {{
|
|
126
|
+
background: transparent; color: {t.muted};
|
|
127
|
+
border: 1px solid transparent; border-radius: 8px; padding: 7px 10px;
|
|
128
|
+
font-size: {base}px; /* explicit so buttons follow the text-size setting */
|
|
129
|
+
}}
|
|
130
|
+
QToolButton:hover, QPushButton:hover {{ background: {t.bg3}; color: {t.text}; border-color: {t.line2}; }}
|
|
131
|
+
QToolButton:pressed, QPushButton:pressed {{ background: {t.bg2}; }}
|
|
132
|
+
QToolButton:checked {{ background: {t.accent_soft}; color: {t.accent}; border-color: {t.accent}; }}
|
|
133
|
+
|
|
134
|
+
/* segmented toolbar clusters — File / Tools / Zoom sit in rounded inset trays */
|
|
135
|
+
QFrame#TbGroup {{ background: {t.bg2}; border: 1px solid {t.line}; border-radius: 10px; }}
|
|
136
|
+
QFrame#TbGroup QToolButton {{
|
|
137
|
+
background: transparent; color: {t.muted};
|
|
138
|
+
border: 1px solid transparent; border-radius: 7px; padding: 6px 8px;
|
|
139
|
+
}}
|
|
140
|
+
QFrame#TbGroup QToolButton:hover {{ background: {t.panel}; color: {t.text}; border-color: {t.line2}; }}
|
|
141
|
+
QFrame#TbGroup QToolButton:pressed {{ background: {t.bg3}; }}
|
|
142
|
+
QFrame#TbGroup QToolButton:checked {{
|
|
143
|
+
background: {t.accent_soft}; color: {t.accent}; border-color: {t.accent}; }}
|
|
144
|
+
|
|
145
|
+
/* Run/Stop is now a single custom-painted circular RunButton (ui/run_button.py). */
|
|
146
|
+
|
|
147
|
+
/* Project navigator — a centred chip in the toolbar that names the current project. */
|
|
148
|
+
QToolButton#NavBtn {{
|
|
149
|
+
background: {t.panel2}; color: {t.text}; font-weight: 600;
|
|
150
|
+
border: 1px solid {t.line}; border-radius: 9px;
|
|
151
|
+
padding: 5px 14px 5px 10px;
|
|
152
|
+
}}
|
|
153
|
+
QToolButton#NavBtn:hover {{ border-color: {t.accent}; color: {t.accent}; }}
|
|
154
|
+
QToolButton#NavBtn::menu-indicator {{ image: none; width: 0; }}
|
|
155
|
+
|
|
156
|
+
QPushButton#Primary {{ background: {t.success}; color: #ffffff; border: none; font-weight: 500; }}
|
|
157
|
+
QPushButton#Primary:hover {{ background: {t.success}; }}
|
|
158
|
+
QPushButton#Danger {{ color: {t.danger}; }}
|
|
159
|
+
QPushButton#Accent {{ background: {t.accent}; color: #ffffff; border: none; font-weight: 500; }}
|
|
160
|
+
|
|
161
|
+
/* GINI mode buttons — clear pressed/unpressed (toggle) state with an accent glow */
|
|
162
|
+
QPushButton#ModeBtn {{
|
|
163
|
+
background: {t.bg3}; color: {t.muted};
|
|
164
|
+
border: 1px solid {t.line}; border-radius: 13px;
|
|
165
|
+
padding: 5px 12px; font-weight: 500; font-size: {base}px;
|
|
166
|
+
}}
|
|
167
|
+
QPushButton#ModeBtn:hover {{ color: {t.text}; border-color: {t.accent}; }}
|
|
168
|
+
QPushButton#ModeBtn:checked {{
|
|
169
|
+
background: {t.accent_soft}; color: {t.accent};
|
|
170
|
+
border: 1px solid {t.accent}; font-weight: 600;
|
|
171
|
+
}}
|
|
172
|
+
QPushButton#WizardBtn:checked {{
|
|
173
|
+
background: {t.accent_soft}; color: {t.accent2}; border: 1px solid {t.accent2};
|
|
174
|
+
}}
|
|
175
|
+
|
|
176
|
+
/* small toggle chips — requirement chips in the Wizard, follow-up suggestions */
|
|
177
|
+
QPushButton#Chip {{
|
|
178
|
+
background: {t.bg3}; color: {t.muted};
|
|
179
|
+
border: 1px solid {t.line}; border-radius: 12px;
|
|
180
|
+
padding: 4px 11px; font-weight: 500; font-size: {base}px;
|
|
181
|
+
}}
|
|
182
|
+
QPushButton#Chip:hover {{ color: {t.text}; border-color: {t.accent}; }}
|
|
183
|
+
QPushButton#Chip:checked {{
|
|
184
|
+
background: {t.accent_soft}; color: {t.accent};
|
|
185
|
+
border: 1px solid {t.accent}; font-weight: 600;
|
|
186
|
+
}}
|
|
187
|
+
|
|
188
|
+
/* Wizard goal banner — the "🎯 Building: …" objective card */
|
|
189
|
+
QWidget#GoalBanner {{
|
|
190
|
+
background: {t.accent_soft}; border: 1.4px solid {t.accent}; border-radius: 11px;
|
|
191
|
+
}}
|
|
192
|
+
|
|
193
|
+
QLineEdit, QPlainTextEdit, QTextEdit, QComboBox, QSpinBox {{
|
|
194
|
+
background: {t.bg3}; color: {t.text};
|
|
195
|
+
border: 1px solid {t.line}; border-radius: 7px; padding: 6px 9px;
|
|
196
|
+
selection-background-color: {t.accent};
|
|
197
|
+
}}
|
|
198
|
+
QLineEdit:focus, QPlainTextEdit:focus, QTextEdit:focus, QComboBox:focus {{ border-color: {t.accent}; }}
|
|
199
|
+
QComboBox::drop-down {{ border: none; width: 18px; }}
|
|
200
|
+
|
|
201
|
+
QLabel#PanelHead {{ color: {t.faint}; font-size: {small}px; font-weight: 600;
|
|
202
|
+
letter-spacing: 1px; text-transform: uppercase; }}
|
|
203
|
+
QLabel#Muted {{ color: {t.muted}; }}
|
|
204
|
+
QLabel#Faint {{ color: {t.faint}; }}
|
|
205
|
+
|
|
206
|
+
QListWidget, QTreeWidget, QTreeView, QListView {{
|
|
207
|
+
background: transparent; border: none; }}
|
|
208
|
+
QListWidget::item, QTreeWidget::item {{
|
|
209
|
+
border-radius: 7px; padding: 5px 6px; margin: 1px 2px; color: {t.text}; }}
|
|
210
|
+
QListWidget::item:hover, QTreeWidget::item:hover {{ background: {t.bg3}; }}
|
|
211
|
+
QListWidget::item:selected, QTreeWidget::item:selected {{
|
|
212
|
+
background: {t.accent_soft}; color: {t.text}; }}
|
|
213
|
+
QHeaderView::section {{ background: transparent; color: {t.faint};
|
|
214
|
+
border: none; border-bottom: 1px solid {t.line}; padding: 4px 6px; }}
|
|
215
|
+
|
|
216
|
+
QScrollArea {{ background: transparent; border: none; }}
|
|
217
|
+
QScrollArea > QWidget > QWidget {{ background: transparent; }}
|
|
218
|
+
QAbstractScrollArea {{ background: transparent; }}
|
|
219
|
+
QTabWidget::pane {{ background: {t.panel}; border: none; border-top: 1px solid {t.line}; }}
|
|
220
|
+
QTabWidget > QWidget {{ background: {t.panel}; }}
|
|
221
|
+
QTabBar::tab {{ background: transparent; color: {t.muted}; padding: 7px 12px;
|
|
222
|
+
border: none; border-bottom: 2px solid transparent; }}
|
|
223
|
+
QTabBar::tab:selected {{ color: {t.text}; border-bottom-color: {t.accent}; }}
|
|
224
|
+
QTabBar::tab:hover {{ color: {t.text}; }}
|
|
225
|
+
|
|
226
|
+
QDockWidget {{ titlebar-close-icon: none; titlebar-normal-icon: none; }}
|
|
227
|
+
QDockWidget::title {{ background: {t.panel}; color: {t.faint};
|
|
228
|
+
padding: 8px 12px; border-bottom: 1px solid {t.line}; }}
|
|
229
|
+
|
|
230
|
+
QMenuBar {{ background: {t.panel}; border-bottom: 1px solid {t.line}; padding: 3px 6px; }}
|
|
231
|
+
QMenuBar::item {{ background: transparent; padding: 5px 10px; border-radius: 6px; color: {t.muted}; }}
|
|
232
|
+
QMenuBar::item:selected {{ background: {t.bg3}; color: {t.text}; }}
|
|
233
|
+
QMenu {{ background: {t.panel2}; border: 1px solid {t.line}; border-radius: 8px; padding: 5px; }}
|
|
234
|
+
QMenu::item {{ padding: 6px 22px 6px 14px; border-radius: 6px; color: {t.text}; }}
|
|
235
|
+
QMenu::item:selected {{ background: {t.accent_soft}; }}
|
|
236
|
+
|
|
237
|
+
QStatusBar {{ background: {t.panel}; border-top: 1px solid {t.line}; color: {t.muted}; }}
|
|
238
|
+
QStatusBar::item {{ border: none; }}
|
|
239
|
+
|
|
240
|
+
QScrollBar:vertical {{ background: transparent; width: 10px; margin: 2px; }}
|
|
241
|
+
QScrollBar::handle:vertical {{ background: {t.line2}; border-radius: 5px; min-height: 24px; }}
|
|
242
|
+
QScrollBar::handle:vertical:hover {{ background: {t.muted}; }}
|
|
243
|
+
QScrollBar:horizontal {{ background: transparent; height: 10px; margin: 2px; }}
|
|
244
|
+
QScrollBar::handle:horizontal {{ background: {t.line2}; border-radius: 5px; min-width: 24px; }}
|
|
245
|
+
QScrollBar::add-line, QScrollBar::sub-line {{ height: 0; width: 0; }}
|
|
246
|
+
QScrollBar::add-page, QScrollBar::sub-page {{ background: transparent; }}
|
|
247
|
+
QAbstractScrollArea::corner {{ background: {t.bg}; border: none; }}
|
|
248
|
+
|
|
249
|
+
QToolTip {{ background: {t.panel2}; color: {t.text};
|
|
250
|
+
border: 1px solid {t.line}; border-radius: 6px; padding: 5px 8px; }}
|
|
251
|
+
QSplitter::handle {{ background: {t.line}; }}
|
|
252
|
+
"""
|
|
253
|
+
|
|
254
|
+
|
|
255
|
+
class ThemeManager(QObject):
|
|
256
|
+
themeChanged = Signal(str)
|
|
257
|
+
|
|
258
|
+
def __init__(self, app: QApplication, name: str = "dark", font_scale: float = 1.0) -> None:
|
|
259
|
+
super().__init__()
|
|
260
|
+
self._app = app
|
|
261
|
+
self.theme: Theme = get_theme(name)
|
|
262
|
+
self.font_scale = font_scale or 1.0
|
|
263
|
+
# Fusion respects QPalette + QSS identically on macOS/Linux/Windows, so the
|
|
264
|
+
# dark theme reaches every widget (the native macOS style does not).
|
|
265
|
+
self._app.setStyle("Fusion")
|
|
266
|
+
|
|
267
|
+
def apply(self) -> None:
|
|
268
|
+
global _ACTIVE_SCALE
|
|
269
|
+
_ACTIVE_SCALE = self.font_scale # publish for custom-painted widgets (canvas, etc.)
|
|
270
|
+
# Scale the app font too, so widgets that read the application font (and any not pinned by
|
|
271
|
+
# the stylesheet) grow with the setting, not just the QSS-styled ones.
|
|
272
|
+
f = self._app.font()
|
|
273
|
+
f.setPointSizeF(10.0 * self.font_scale)
|
|
274
|
+
self._app.setFont(f)
|
|
275
|
+
self._app.setPalette(build_palette(self.theme))
|
|
276
|
+
self._app.setStyleSheet(build_qss(self.theme, self.font_scale))
|
|
277
|
+
|
|
278
|
+
def set_theme(self, name: str) -> None:
|
|
279
|
+
self.theme = get_theme(name)
|
|
280
|
+
self.apply()
|
|
281
|
+
self.themeChanged.emit(self.theme.name)
|
|
282
|
+
|
|
283
|
+
def set_font_scale(self, scale: float) -> None:
|
|
284
|
+
"""Change the UI text size live (Settings → Appearance)."""
|
|
285
|
+
if abs((scale or 1.0) - self.font_scale) < 1e-3:
|
|
286
|
+
return
|
|
287
|
+
self.font_scale = scale or 1.0
|
|
288
|
+
self.apply()
|
|
289
|
+
# nudge every theme-aware widget to re-render at the new size (palette headers, dashboard,
|
|
290
|
+
# canvas cards, pills all re-style / repaint on this signal).
|
|
291
|
+
self.themeChanged.emit(self.theme.name)
|
gini/ui/theme/tokens.py
ADDED
|
@@ -0,0 +1,194 @@
|
|
|
1
|
+
"""Themeable design tokens.
|
|
2
|
+
|
|
3
|
+
A Theme is a flat bag of color tokens. The QSS generator and the custom-painted
|
|
4
|
+
canvas both read from it, so re-theming the whole app is a matter of swapping one
|
|
5
|
+
Theme object. New themes can be added by appending to THEMES — including ones
|
|
6
|
+
loaded from JSON at runtime.
|
|
7
|
+
"""
|
|
8
|
+
from __future__ import annotations
|
|
9
|
+
|
|
10
|
+
from dataclasses import dataclass, field
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
@dataclass(frozen=True)
|
|
14
|
+
class Theme:
|
|
15
|
+
name: str
|
|
16
|
+
dark: bool
|
|
17
|
+
# surfaces
|
|
18
|
+
bg: str
|
|
19
|
+
bg2: str
|
|
20
|
+
bg3: str
|
|
21
|
+
panel: str
|
|
22
|
+
panel2: str
|
|
23
|
+
# borders
|
|
24
|
+
line: str
|
|
25
|
+
line2: str
|
|
26
|
+
# text
|
|
27
|
+
text: str
|
|
28
|
+
muted: str
|
|
29
|
+
faint: str
|
|
30
|
+
# primary accent
|
|
31
|
+
accent: str
|
|
32
|
+
accent2: str
|
|
33
|
+
accent_soft: str
|
|
34
|
+
# semantic
|
|
35
|
+
success: str
|
|
36
|
+
success_soft: str
|
|
37
|
+
warning: str
|
|
38
|
+
warning_soft: str
|
|
39
|
+
danger: str
|
|
40
|
+
danger_soft: str
|
|
41
|
+
# canvas
|
|
42
|
+
grid: str
|
|
43
|
+
# category accents (icon recolor + node accent), keyed by Accent.value
|
|
44
|
+
accents: dict[str, str] = field(default_factory=dict)
|
|
45
|
+
# fill for element cards ON the canvas — a lighter, theme-tinted surface so nodes
|
|
46
|
+
# pop off the background (empty = fall back to panel2, i.e. the dark themes' behaviour)
|
|
47
|
+
node: str = ""
|
|
48
|
+
# depth + motion
|
|
49
|
+
shadow: str = "rgba(0,0,0,120)"
|
|
50
|
+
elevation: int = 22 # node drop-shadow blur radius (deeper = more depth)
|
|
51
|
+
dur_fast: int = 120 # ms — hover/press
|
|
52
|
+
dur_base: int = 200 # ms — transitions
|
|
53
|
+
|
|
54
|
+
def accent_for(self, key: str) -> str:
|
|
55
|
+
return self.accents.get(key, self.accent)
|
|
56
|
+
|
|
57
|
+
def node_fill(self) -> str:
|
|
58
|
+
"""The canvas element-card fill (see `node`)."""
|
|
59
|
+
return self.node or self.panel2
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
_CATEGORY_DARK = {
|
|
63
|
+
"blue": "#4c8dff", "green": "#3fb950", "purple": "#a371f7", "teal": "#39c5cf",
|
|
64
|
+
"cyan": "#2dd4bf", "indigo": "#7c8cff", "amber": "#e3a008", "pink": "#f778ba",
|
|
65
|
+
"slate": "#8b98a8", "orange": "#f0883e", "red": "#f85149",
|
|
66
|
+
}
|
|
67
|
+
_CATEGORY_LIGHT = {
|
|
68
|
+
"blue": "#2f6fe0", "green": "#1a7f37", "purple": "#8250df", "teal": "#1b7c83",
|
|
69
|
+
"cyan": "#0e9384", "indigo": "#4f57d2", "amber": "#9a6700", "pink": "#bf3989",
|
|
70
|
+
"slate": "#57606a", "orange": "#bc4c00", "red": "#cf222e",
|
|
71
|
+
}
|
|
72
|
+
_CATEGORY_BRAND = {
|
|
73
|
+
"blue": "#5aa0ff", "green": "#46c98a", "purple": "#b78cff", "teal": "#43d6c4",
|
|
74
|
+
"cyan": "#34e0c8", "indigo": "#8aa0ff", "amber": "#f0b429", "pink": "#ff8ad0",
|
|
75
|
+
"slate": "#9fb0c8", "orange": "#ff9e4f", "red": "#ff6b66",
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
|
|
79
|
+
DARK = Theme(
|
|
80
|
+
name="Dark", dark=True,
|
|
81
|
+
bg="#0e1116", bg2="#151a21", bg3="#1b212b", panel="#11161d", panel2="#161c24",
|
|
82
|
+
line="#232b36", line2="#2d3744",
|
|
83
|
+
text="#e6edf3", muted="#9aa7b4", faint="#697682",
|
|
84
|
+
accent="#4c8dff", accent2="#2f6fe0", accent_soft="rgba(76,141,255,40)",
|
|
85
|
+
success="#3fb950", success_soft="rgba(63,185,80,38)",
|
|
86
|
+
warning="#d29922", warning_soft="rgba(210,153,34,38)",
|
|
87
|
+
danger="#f85149", danger_soft="rgba(248,81,73,38)",
|
|
88
|
+
grid="rgba(255,255,255,12)",
|
|
89
|
+
shadow="rgba(0,0,0,150)",
|
|
90
|
+
accents=_CATEGORY_DARK,
|
|
91
|
+
)
|
|
92
|
+
|
|
93
|
+
LIGHT = Theme(
|
|
94
|
+
name="Light", dark=False,
|
|
95
|
+
bg="#eef1f5", bg2="#e7ebf1", bg3="#dfe4ec", panel="#ffffff", panel2="#f6f8fb",
|
|
96
|
+
line="#d7dde6", line2="#c4ccd8",
|
|
97
|
+
text="#131a23", muted="#3f4b5a", faint="#5e6b7b",
|
|
98
|
+
accent="#2f6fe0", accent2="#2560c8", accent_soft="rgba(47,111,224,28)",
|
|
99
|
+
success="#1a7f37", success_soft="rgba(26,127,55,26)",
|
|
100
|
+
warning="#9a6700", warning_soft="rgba(154,103,0,26)",
|
|
101
|
+
danger="#cf222e", danger_soft="rgba(207,34,46,26)",
|
|
102
|
+
grid="rgba(20,30,50,16)",
|
|
103
|
+
shadow="rgba(60,80,120,50)",
|
|
104
|
+
node="#fdfeff",
|
|
105
|
+
accents=_CATEGORY_LIGHT,
|
|
106
|
+
)
|
|
107
|
+
|
|
108
|
+
BRAND = Theme(
|
|
109
|
+
name="GINI Brand", dark=True,
|
|
110
|
+
bg="#0b1020", bg2="#121833", bg3="#18204a", panel="#0e1428", panel2="#141c3a",
|
|
111
|
+
line="#23305c", line2="#2f3e74",
|
|
112
|
+
text="#eaf0ff", muted="#9fb0d6", faint="#6f80ad",
|
|
113
|
+
accent="#43d6c4", accent2="#2bb6a6", accent_soft="rgba(67,214,196,40)",
|
|
114
|
+
success="#46c98a", success_soft="rgba(70,201,138,38)",
|
|
115
|
+
warning="#f0b429", warning_soft="rgba(240,180,41,38)",
|
|
116
|
+
danger="#ff6b6b", danger_soft="rgba(255,107,107,38)",
|
|
117
|
+
grid="rgba(120,160,255,16)",
|
|
118
|
+
shadow="rgba(0,0,25,160)",
|
|
119
|
+
accents=_CATEGORY_BRAND,
|
|
120
|
+
)
|
|
121
|
+
|
|
122
|
+
HIGH_CONTRAST = Theme(
|
|
123
|
+
name="High Contrast", dark=True,
|
|
124
|
+
bg="#000000", bg2="#0a0a0a", bg3="#161616", panel="#000000", panel2="#0d0d0d",
|
|
125
|
+
line="#5a5a5a", line2="#7c7c7c",
|
|
126
|
+
text="#ffffff", muted="#d6d6d6", faint="#a6a6a6",
|
|
127
|
+
accent="#5aa3ff", accent2="#3a86ff", accent_soft="rgba(90,163,255,60)",
|
|
128
|
+
success="#3fe06a", success_soft="rgba(63,224,106,60)",
|
|
129
|
+
warning="#ffcc33", warning_soft="rgba(255,204,51,60)",
|
|
130
|
+
danger="#ff5a5a", danger_soft="rgba(255,90,90,60)",
|
|
131
|
+
grid="rgba(255,255,255,30)",
|
|
132
|
+
shadow="rgba(0,0,0,210)",
|
|
133
|
+
accents={"blue": "#5aa3ff", "green": "#3fe06a", "purple": "#c08cff",
|
|
134
|
+
"teal": "#3fe0d0", "cyan": "#3fe0c0", "indigo": "#9aa8ff",
|
|
135
|
+
"amber": "#ffcc33", "pink": "#ff7ad0", "slate": "#c8d0d8",
|
|
136
|
+
"orange": "#ff9e4f", "red": "#ff6b66"},
|
|
137
|
+
)
|
|
138
|
+
|
|
139
|
+
|
|
140
|
+
# --- light family: warmer/cooler tints of the Light theme ------------------- #
|
|
141
|
+
SAND = Theme(
|
|
142
|
+
name="Sand", dark=False,
|
|
143
|
+
bg="#f4efe4", bg2="#ece5d6", bg3="#e3dac7", panel="#fdfaf3", panel2="#f4efe4",
|
|
144
|
+
line="#ddd2be", line2="#cabca3",
|
|
145
|
+
text="#2b2418", muted="#5d5240", faint="#7e715c",
|
|
146
|
+
accent="#b46e28", accent2="#985c1c", accent_soft="rgba(180,110,40,30)",
|
|
147
|
+
success="#4c7a2f", success_soft="rgba(76,122,47,26)",
|
|
148
|
+
warning="#9a6700", warning_soft="rgba(154,103,0,26)",
|
|
149
|
+
danger="#c0392b", danger_soft="rgba(192,57,43,26)",
|
|
150
|
+
grid="rgba(90,70,35,16)",
|
|
151
|
+
shadow="rgba(90,70,40,55)",
|
|
152
|
+
node="#fffdf6",
|
|
153
|
+
accents=_CATEGORY_LIGHT,
|
|
154
|
+
)
|
|
155
|
+
|
|
156
|
+
BLUE = Theme(
|
|
157
|
+
name="Blue", dark=False,
|
|
158
|
+
bg="#eef2f9", bg2="#e4ecf6", bg3="#d9e3f2", panel="#ffffff", panel2="#f3f7fc",
|
|
159
|
+
line="#d2ddec", line2="#becde2",
|
|
160
|
+
text="#111a2b", muted="#3c4a63", faint="#5b6a85",
|
|
161
|
+
accent="#2f6fe0", accent2="#2560c8", accent_soft="rgba(47,111,224,28)",
|
|
162
|
+
success="#1a7f37", success_soft="rgba(26,127,55,26)",
|
|
163
|
+
warning="#9a6700", warning_soft="rgba(154,103,0,26)",
|
|
164
|
+
danger="#cf222e", danger_soft="rgba(207,34,46,26)",
|
|
165
|
+
grid="rgba(30,60,120,16)",
|
|
166
|
+
shadow="rgba(40,70,130,55)",
|
|
167
|
+
node="#fafdff",
|
|
168
|
+
accents=_CATEGORY_LIGHT,
|
|
169
|
+
)
|
|
170
|
+
|
|
171
|
+
GREEN = Theme(
|
|
172
|
+
name="Green", dark=False,
|
|
173
|
+
bg="#eef4ee", bg2="#e4eee4", bg3="#d8e7d8", panel="#fbfdfb", panel2="#f0f7f0",
|
|
174
|
+
line="#d1e0d1", line2="#bcd0bc",
|
|
175
|
+
text="#14231a", muted="#3e5245", faint="#5d7163",
|
|
176
|
+
accent="#2e8b57", accent2="#237045", accent_soft="rgba(46,139,87,28)",
|
|
177
|
+
success="#1a7f37", success_soft="rgba(26,127,55,26)",
|
|
178
|
+
warning="#9a6700", warning_soft="rgba(154,103,0,26)",
|
|
179
|
+
danger="#cf222e", danger_soft="rgba(207,34,46,26)",
|
|
180
|
+
grid="rgba(30,90,50,16)",
|
|
181
|
+
shadow="rgba(40,90,60,50)",
|
|
182
|
+
node="#f9fefb",
|
|
183
|
+
accents=_CATEGORY_LIGHT,
|
|
184
|
+
)
|
|
185
|
+
|
|
186
|
+
|
|
187
|
+
THEMES: dict[str, Theme] = {t.name.lower(): t for t in
|
|
188
|
+
(DARK, LIGHT, BRAND, HIGH_CONTRAST, SAND, BLUE, GREEN)}
|
|
189
|
+
# friendly aliases
|
|
190
|
+
THEMES["brand"] = BRAND
|
|
191
|
+
|
|
192
|
+
|
|
193
|
+
def get_theme(name: str) -> Theme:
|
|
194
|
+
return THEMES.get(name.lower(), DARK)
|