labforge 0.1.0__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.
- labforge/__init__.py +17 -0
- labforge/controls.py +278 -0
- labforge/figures.py +92 -0
- labforge/lab.py +165 -0
- labforge/mathtext.py +140 -0
- labforge/pages/__init__.py +6 -0
- labforge/pages/analysis.py +99 -0
- labforge/pages/panel.py +109 -0
- labforge/pages/scroll.py +61 -0
- labforge/pages/simulation.py +71 -0
- labforge/pages/theory.py +40 -0
- labforge/pages/visualization.py +30 -0
- labforge/param.py +186 -0
- labforge/registry.py +36 -0
- labforge/scan.py +75 -0
- labforge/shell.py +220 -0
- labforge/state.py +74 -0
- labforge/theme.py +323 -0
- labforge/ui.py +167 -0
- labforge-0.1.0.dist-info/METADATA +232 -0
- labforge-0.1.0.dist-info/RECORD +23 -0
- labforge-0.1.0.dist-info/WHEEL +4 -0
- labforge-0.1.0.dist-info/licenses/LICENSE +21 -0
labforge/scan.py
ADDED
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
"""
|
|
2
|
+
The scan engine: run the worker once, or over a cartesian grid of scan axes.
|
|
3
|
+
|
|
4
|
+
Pure Python, no Flet — unit-testable without a UI.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
import itertools
|
|
8
|
+
from dataclasses import dataclass
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
@dataclass
|
|
12
|
+
class ScanResult:
|
|
13
|
+
"""
|
|
14
|
+
The collected output of a parameter scan.
|
|
15
|
+
|
|
16
|
+
A list of (params, result) records rather than a tuple-keyed dict: records
|
|
17
|
+
are self-describing (each carries its full kwarg dict), preserve cartesian
|
|
18
|
+
order, and avoid float-equality lookups.
|
|
19
|
+
|
|
20
|
+
Parameters
|
|
21
|
+
----------
|
|
22
|
+
keys: list
|
|
23
|
+
The scanned kwarg names, in control order.
|
|
24
|
+
points: list
|
|
25
|
+
(params dict, worker return) tuples, in cartesian-product order.
|
|
26
|
+
"""
|
|
27
|
+
|
|
28
|
+
keys: list
|
|
29
|
+
points: list
|
|
30
|
+
|
|
31
|
+
def __iter__(self):
|
|
32
|
+
return iter(self.points)
|
|
33
|
+
|
|
34
|
+
def __len__(self):
|
|
35
|
+
return len(self.points)
|
|
36
|
+
|
|
37
|
+
def values(self):
|
|
38
|
+
"""The worker returns alone, in cartesian-product order."""
|
|
39
|
+
return [value for _, value in self.points]
|
|
40
|
+
|
|
41
|
+
def axis(self, key):
|
|
42
|
+
"""The distinct values one scanned kwarg took, in entry order."""
|
|
43
|
+
return list(dict.fromkeys(params[key] for params, _ in self.points))
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
def run_worker(func, values):
|
|
47
|
+
"""
|
|
48
|
+
Call the worker once, or over the cartesian grid of its scan axes.
|
|
49
|
+
|
|
50
|
+
The worker always receives scalars: an axis is expanded into one call per
|
|
51
|
+
grid point, never passed through as a list. Viz and analysis functions tell
|
|
52
|
+
the two cases apart with isinstance(data, ScanResult).
|
|
53
|
+
|
|
54
|
+
Parameters
|
|
55
|
+
----------
|
|
56
|
+
func: callable
|
|
57
|
+
The registered worker.
|
|
58
|
+
values: dict
|
|
59
|
+
Parsed control values; list-valued entries are scan axes.
|
|
60
|
+
|
|
61
|
+
Returns
|
|
62
|
+
-------
|
|
63
|
+
The worker's bare return for a pure-scalar call, or a ScanResult when any
|
|
64
|
+
axis is a list.
|
|
65
|
+
"""
|
|
66
|
+
axes = {key: value for key, value in values.items() if isinstance(value, list)}
|
|
67
|
+
if not axes:
|
|
68
|
+
return func(**values)
|
|
69
|
+
|
|
70
|
+
fixed = {key: value for key, value in values.items() if key not in axes}
|
|
71
|
+
points = []
|
|
72
|
+
for combo in itertools.product(*axes.values()):
|
|
73
|
+
params = dict(fixed, **dict(zip(axes.keys(), combo)))
|
|
74
|
+
points.append((params, func(**params)))
|
|
75
|
+
return ScanResult(keys=list(axes.keys()), points=points)
|
labforge/shell.py
ADDED
|
@@ -0,0 +1,220 @@
|
|
|
1
|
+
"""
|
|
2
|
+
The app chrome: top bar, navigation rail or scroll layout, and the single
|
|
3
|
+
LabState every page shares.
|
|
4
|
+
"""
|
|
5
|
+
|
|
6
|
+
import flet as ft
|
|
7
|
+
|
|
8
|
+
from . import theme as themes
|
|
9
|
+
from . import ui
|
|
10
|
+
from .pages import analysis, scroll, simulation, theory, visualization
|
|
11
|
+
from .state import LabState
|
|
12
|
+
|
|
13
|
+
PAGES = [
|
|
14
|
+
("Theory", ft.Icons.MENU_BOOK, theory.build),
|
|
15
|
+
("Simulation", ft.Icons.TERMINAL, simulation.build),
|
|
16
|
+
("Visualization", ft.Icons.AUTO_GRAPH, visualization.build),
|
|
17
|
+
("Analysis", ft.Icons.TROUBLESHOOT, analysis.build),
|
|
18
|
+
]
|
|
19
|
+
|
|
20
|
+
LAYOUTS = ("pages", "scroll")
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
def color_scheme(theme):
|
|
24
|
+
"""
|
|
25
|
+
Map a Theme onto the Flet ColorScheme slots the pages resolve through.
|
|
26
|
+
|
|
27
|
+
Pages never name a hex; they style themselves with ft.Colors tokens, so this
|
|
28
|
+
mapping is the only place the palette reaches the UI.
|
|
29
|
+
"""
|
|
30
|
+
return ft.ColorScheme(
|
|
31
|
+
primary=theme.accent,
|
|
32
|
+
on_primary=theme.on_accent,
|
|
33
|
+
primary_container=theme.accent_dim,
|
|
34
|
+
on_primary_container=theme.on_accent_dim,
|
|
35
|
+
secondary=theme.accent,
|
|
36
|
+
on_secondary=theme.on_accent,
|
|
37
|
+
secondary_container=theme.accent_dim,
|
|
38
|
+
on_secondary_container=theme.on_accent_dim,
|
|
39
|
+
surface=theme.surface,
|
|
40
|
+
on_surface=theme.on_surface,
|
|
41
|
+
on_surface_variant=theme.on_surface_variant,
|
|
42
|
+
surface_container_lowest=theme.surface_lowest,
|
|
43
|
+
surface_container_low=theme.surface_low,
|
|
44
|
+
surface_container=theme.surface_container,
|
|
45
|
+
surface_container_high=theme.surface_high,
|
|
46
|
+
surface_container_highest=theme.surface_highest,
|
|
47
|
+
outline=theme.outline,
|
|
48
|
+
outline_variant=theme.outline_variant,
|
|
49
|
+
surface_tint=theme.accent,
|
|
50
|
+
)
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
def build_main(lab, layout="pages", theme=themes.DEFAULT):
|
|
54
|
+
"""
|
|
55
|
+
Return the Flet entry point — a main(page) callable — for a Lab.
|
|
56
|
+
|
|
57
|
+
Parameters
|
|
58
|
+
----------
|
|
59
|
+
lab: Lab
|
|
60
|
+
The validated registrations and branding.
|
|
61
|
+
layout: str
|
|
62
|
+
"pages" for the rail-navigated four-page shell, "scroll" for one
|
|
63
|
+
continuous scrolling page.
|
|
64
|
+
theme: str
|
|
65
|
+
A key of theme.THEMES. Selected here, once, before the app serves —
|
|
66
|
+
figures and equations read it back through theme.active().
|
|
67
|
+
|
|
68
|
+
Returns
|
|
69
|
+
-------
|
|
70
|
+
A main(page) callable; also the headless seam the page-tree tests drive.
|
|
71
|
+
"""
|
|
72
|
+
if layout not in LAYOUTS:
|
|
73
|
+
raise ValueError(f"layout must be one of {sorted(LAYOUTS)}, got {layout!r}.")
|
|
74
|
+
|
|
75
|
+
palette = themes.use(theme)
|
|
76
|
+
scheme = color_scheme(palette)
|
|
77
|
+
|
|
78
|
+
# Flet calls main(page) once per connection, so every browser tab that opens
|
|
79
|
+
# the served app gets its own LabState. Nothing may be cached out here
|
|
80
|
+
# except immutable config.
|
|
81
|
+
def main(page):
|
|
82
|
+
page.title = lab.page_title
|
|
83
|
+
# The mode matches the palette so widget defaults the scheme does not
|
|
84
|
+
# name (shadows, ripples, error tones) derive with the right brightness.
|
|
85
|
+
page.theme_mode = ft.ThemeMode.DARK if palette.mode == "dark" else ft.ThemeMode.LIGHT
|
|
86
|
+
# The seed fills the scheme slots color_scheme leaves unset; the same
|
|
87
|
+
# Theme is set on both slots so the explicit surfaces win regardless of
|
|
88
|
+
# which one Flet consults for the mode.
|
|
89
|
+
page_theme = ft.Theme(
|
|
90
|
+
color_scheme_seed=palette.accent, color_scheme=scheme, font_family=ui.FONT_BODY
|
|
91
|
+
)
|
|
92
|
+
page.theme = page_theme
|
|
93
|
+
page.dark_theme = page_theme
|
|
94
|
+
page.window.width = 1120
|
|
95
|
+
page.window.height = 840
|
|
96
|
+
page.window.min_width = 900
|
|
97
|
+
page.window.min_height = 640
|
|
98
|
+
|
|
99
|
+
# The one state object every page reads from and writes to.
|
|
100
|
+
state = LabState(lab)
|
|
101
|
+
|
|
102
|
+
# App mark: the lab's icon (a volume mark by default) on a sharp accent tile.
|
|
103
|
+
app_mark = ft.Container(
|
|
104
|
+
width=30,
|
|
105
|
+
height=30,
|
|
106
|
+
border_radius=2,
|
|
107
|
+
bgcolor=ft.Colors.PRIMARY,
|
|
108
|
+
alignment=ft.Alignment.CENTER,
|
|
109
|
+
content=ft.Icon(lab.icon or ft.Icons.VIEW_IN_AR, size=18, color=ft.Colors.ON_PRIMARY),
|
|
110
|
+
)
|
|
111
|
+
|
|
112
|
+
# Thin header strip: mark and tracked mono-caps title on the left, a
|
|
113
|
+
# factual mono readout (the registered worker) on the right.
|
|
114
|
+
top_bar = ft.Container(
|
|
115
|
+
padding=ft.Padding.symmetric(horizontal=20, vertical=10),
|
|
116
|
+
bgcolor=ft.Colors.SURFACE_CONTAINER_LOWEST,
|
|
117
|
+
content=ft.Row(
|
|
118
|
+
alignment=ft.MainAxisAlignment.SPACE_BETWEEN,
|
|
119
|
+
vertical_alignment=ft.CrossAxisAlignment.CENTER,
|
|
120
|
+
controls=[
|
|
121
|
+
ft.Row(
|
|
122
|
+
spacing=12,
|
|
123
|
+
vertical_alignment=ft.CrossAxisAlignment.CENTER,
|
|
124
|
+
controls=[
|
|
125
|
+
app_mark,
|
|
126
|
+
ft.Text(
|
|
127
|
+
lab.title.upper(),
|
|
128
|
+
style=ft.TextStyle(
|
|
129
|
+
size=14,
|
|
130
|
+
weight=ft.FontWeight.W_700,
|
|
131
|
+
letter_spacing=3,
|
|
132
|
+
font_family=ui.FONT_MONO,
|
|
133
|
+
),
|
|
134
|
+
),
|
|
135
|
+
],
|
|
136
|
+
),
|
|
137
|
+
ft.Text(
|
|
138
|
+
f"WORKER · {lab.worker.func.__name__.upper()}",
|
|
139
|
+
style=ft.TextStyle(
|
|
140
|
+
size=11,
|
|
141
|
+
letter_spacing=2,
|
|
142
|
+
font_family=ui.FONT_MONO,
|
|
143
|
+
color=ft.Colors.ON_SURFACE_VARIANT,
|
|
144
|
+
),
|
|
145
|
+
),
|
|
146
|
+
],
|
|
147
|
+
),
|
|
148
|
+
)
|
|
149
|
+
|
|
150
|
+
if layout == "scroll":
|
|
151
|
+
# One continuous page under the top bar; no rail, no navigation.
|
|
152
|
+
body = ft.Container(expand=True, padding=24, content=scroll.build(state, page))
|
|
153
|
+
else:
|
|
154
|
+
# Right-hand pane; its single child is swapped on navigation.
|
|
155
|
+
content = ft.Container(expand=True, padding=24)
|
|
156
|
+
|
|
157
|
+
def show_page(index):
|
|
158
|
+
# Rebuild-on-navigate: build fresh so the page re-reads LabState.
|
|
159
|
+
content.content = PAGES[index][2](state, page)
|
|
160
|
+
|
|
161
|
+
def on_nav_change(e):
|
|
162
|
+
show_page(e.control.selected_index)
|
|
163
|
+
page.update()
|
|
164
|
+
|
|
165
|
+
# Spec-sheet rail: numbered mono-caps labels, a squared indicator.
|
|
166
|
+
rail = ft.NavigationRail(
|
|
167
|
+
selected_index=0,
|
|
168
|
+
label_type=ft.NavigationRailLabelType.ALL,
|
|
169
|
+
min_width=100,
|
|
170
|
+
bgcolor=ft.Colors.SURFACE_CONTAINER_LOWEST,
|
|
171
|
+
indicator_color=ft.Colors.PRIMARY_CONTAINER,
|
|
172
|
+
indicator_shape=ft.RoundedRectangleBorder(radius=2),
|
|
173
|
+
selected_label_text_style=ft.TextStyle(
|
|
174
|
+
size=11,
|
|
175
|
+
weight=ft.FontWeight.W_700,
|
|
176
|
+
letter_spacing=1.5,
|
|
177
|
+
font_family=ui.FONT_MONO,
|
|
178
|
+
color=ft.Colors.PRIMARY,
|
|
179
|
+
),
|
|
180
|
+
unselected_label_text_style=ft.TextStyle(
|
|
181
|
+
size=11,
|
|
182
|
+
letter_spacing=1.5,
|
|
183
|
+
font_family=ui.FONT_MONO,
|
|
184
|
+
color=ft.Colors.ON_SURFACE_VARIANT,
|
|
185
|
+
),
|
|
186
|
+
on_change=on_nav_change,
|
|
187
|
+
destinations=[
|
|
188
|
+
ft.NavigationRailDestination(
|
|
189
|
+
icon=icon, label=f"{index + 1:02d} {label.upper()}"
|
|
190
|
+
)
|
|
191
|
+
for index, (label, icon, _) in enumerate(PAGES)
|
|
192
|
+
],
|
|
193
|
+
)
|
|
194
|
+
|
|
195
|
+
# Render the first page before mounting, so no handler fires against
|
|
196
|
+
# an unattached tree.
|
|
197
|
+
show_page(0)
|
|
198
|
+
|
|
199
|
+
body = ft.Row(
|
|
200
|
+
expand=True,
|
|
201
|
+
controls=[
|
|
202
|
+
rail,
|
|
203
|
+
ft.VerticalDivider(width=1, color=ft.Colors.OUTLINE_VARIANT),
|
|
204
|
+
content,
|
|
205
|
+
],
|
|
206
|
+
)
|
|
207
|
+
|
|
208
|
+
page.add(
|
|
209
|
+
ft.Column(
|
|
210
|
+
expand=True,
|
|
211
|
+
spacing=0,
|
|
212
|
+
controls=[
|
|
213
|
+
top_bar,
|
|
214
|
+
ft.Divider(height=1, color=ft.Colors.OUTLINE_VARIANT),
|
|
215
|
+
body,
|
|
216
|
+
],
|
|
217
|
+
)
|
|
218
|
+
)
|
|
219
|
+
|
|
220
|
+
return main
|
labforge/state.py
ADDED
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
"""
|
|
2
|
+
LabState: the mutable workspace every page of a running app shares.
|
|
3
|
+
|
|
4
|
+
Pages are rebuilt fresh on each navigation and the state has no observers, so
|
|
5
|
+
anything that must survive a rebuild lives here.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
import time
|
|
9
|
+
from dataclasses import dataclass, field
|
|
10
|
+
|
|
11
|
+
from .scan import ScanResult, run_worker
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
@dataclass
|
|
15
|
+
class LabState:
|
|
16
|
+
"""
|
|
17
|
+
The mutable workspace of a running Lab.
|
|
18
|
+
|
|
19
|
+
Parameters
|
|
20
|
+
----------
|
|
21
|
+
lab: Lab
|
|
22
|
+
The registrations; read-only from pages.
|
|
23
|
+
worker_values: dict
|
|
24
|
+
Current parsed control values; list-valued entries are scan axes.
|
|
25
|
+
data: object
|
|
26
|
+
The last run's output — the worker's bare return, or a ScanResult.
|
|
27
|
+
n_points: int
|
|
28
|
+
Grid size of the last run (1 for a scalar run, 0 before any run).
|
|
29
|
+
run_summary: str
|
|
30
|
+
Status line shown on the Simulation page.
|
|
31
|
+
viz_values: dict
|
|
32
|
+
{viz title: {kwarg: value}}, persisted across page rebuilds.
|
|
33
|
+
analysis_values: dict
|
|
34
|
+
{analysis title: {kwarg: value}}, persisted across page rebuilds.
|
|
35
|
+
"""
|
|
36
|
+
|
|
37
|
+
lab: object
|
|
38
|
+
worker_values: dict = None
|
|
39
|
+
data: object = None
|
|
40
|
+
n_points: int = 0
|
|
41
|
+
run_summary: str = ""
|
|
42
|
+
viz_values: dict = field(default_factory=dict)
|
|
43
|
+
analysis_values: dict = field(default_factory=dict)
|
|
44
|
+
|
|
45
|
+
def __post_init__(self):
|
|
46
|
+
# Seed every control value from its normalized Param default.
|
|
47
|
+
if self.worker_values is None:
|
|
48
|
+
self.worker_values = defaults(self.lab.worker.params)
|
|
49
|
+
for entry in self.lab.vizzes:
|
|
50
|
+
self.viz_values.setdefault(entry.title, defaults(entry.params))
|
|
51
|
+
for entry in self.lab.analyses:
|
|
52
|
+
self.analysis_values.setdefault(entry.title, defaults(entry.params))
|
|
53
|
+
|
|
54
|
+
def has_data(self):
|
|
55
|
+
"""True once the worker has produced data; gates Viz and Analysis."""
|
|
56
|
+
return self.n_points > 0
|
|
57
|
+
|
|
58
|
+
def run(self):
|
|
59
|
+
"""Run the worker over the current values and record a telemetry line."""
|
|
60
|
+
start = time.perf_counter()
|
|
61
|
+
self.data = run_worker(self.lab.worker.func, self.worker_values)
|
|
62
|
+
self.n_points = len(self.data) if isinstance(self.data, ScanResult) else 1
|
|
63
|
+
elapsed = time.perf_counter() - start
|
|
64
|
+
# Report the grid the author actually asked for, not just the point count.
|
|
65
|
+
axes = sum(1 for value in self.worker_values.values() if isinstance(value, list))
|
|
66
|
+
grid = f"{self.n_points} {'POINT' if self.n_points == 1 else 'POINTS'}"
|
|
67
|
+
if axes:
|
|
68
|
+
grid += f" · {axes} {'AXIS' if axes == 1 else 'AXES'}"
|
|
69
|
+
self.run_summary = f"RUN COMPLETE · {grid} · {elapsed:.2f} s"
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
def defaults(params):
|
|
73
|
+
"""Initial control values for a normalized {name: Param} spec."""
|
|
74
|
+
return {name: param.default for name, param in params.items()}
|
labforge/theme.py
ADDED
|
@@ -0,0 +1,323 @@
|
|
|
1
|
+
"""
|
|
2
|
+
The colour table — chrome, plot palette and equation ink — and the one
|
|
3
|
+
process-wide selection of it.
|
|
4
|
+
|
|
5
|
+
Holding all three in one frozen value is what keeps them from drifting apart.
|
|
6
|
+
Pure hexes, no Flet import: shell.py owns the mapping onto ColorScheme slots, so
|
|
7
|
+
figures.py and mathtext.py read the palette without pulling in the UI layer.
|
|
8
|
+
"""
|
|
9
|
+
|
|
10
|
+
from dataclasses import dataclass
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
@dataclass(frozen=True)
|
|
14
|
+
class Theme:
|
|
15
|
+
"""
|
|
16
|
+
Every colour the app draws, chrome and plots together.
|
|
17
|
+
|
|
18
|
+
Parameters
|
|
19
|
+
----------
|
|
20
|
+
name: str
|
|
21
|
+
The key open(theme=...) selects this theme by.
|
|
22
|
+
note: str
|
|
23
|
+
One-line description, shown by names().
|
|
24
|
+
mode: str
|
|
25
|
+
"dark" or "light"; drives the Flet ThemeMode so widget defaults the
|
|
26
|
+
scheme does not name (shadows, ripples, error tones) match the surfaces.
|
|
27
|
+
accent: str
|
|
28
|
+
The single electric colour reserved for interactive and live things.
|
|
29
|
+
on_accent: str
|
|
30
|
+
Ink drawn on top of accent fills; carries the accent's contrast burden.
|
|
31
|
+
accent_dim: str
|
|
32
|
+
Muted accent fill for selected-but-not-active chrome (the rail indicator).
|
|
33
|
+
on_accent_dim: str
|
|
34
|
+
Ink on accent_dim.
|
|
35
|
+
surface: str
|
|
36
|
+
The page background.
|
|
37
|
+
surface_lowest: str
|
|
38
|
+
Recessed chrome — the top bar and rail.
|
|
39
|
+
surface_low: str
|
|
40
|
+
The lowest raised container step.
|
|
41
|
+
surface_container: str
|
|
42
|
+
The standard card fill.
|
|
43
|
+
surface_high: str
|
|
44
|
+
A raised card step.
|
|
45
|
+
surface_highest: str
|
|
46
|
+
The most raised step; input fills.
|
|
47
|
+
on_surface: str
|
|
48
|
+
Body ink.
|
|
49
|
+
on_surface_variant: str
|
|
50
|
+
Secondary ink — labels, readouts, anything factual but not primary.
|
|
51
|
+
outline: str
|
|
52
|
+
Borders that must read as a boundary.
|
|
53
|
+
outline_variant: str
|
|
54
|
+
Hairline dividers that must not.
|
|
55
|
+
data: str
|
|
56
|
+
Plot colour for the worker's data — matches accent, so a figure reads as
|
|
57
|
+
part of the app.
|
|
58
|
+
model: str
|
|
59
|
+
Plot colour for a model or comparison curve; contrasts with data.
|
|
60
|
+
highlight: str
|
|
61
|
+
Third plot colour, for an emphasised subset.
|
|
62
|
+
ink: str
|
|
63
|
+
Plot and equation ink. Sits on a transparent PNG over surface, so it
|
|
64
|
+
tracks on_surface rather than being pure white.
|
|
65
|
+
grid: str
|
|
66
|
+
Plot grid and spines.
|
|
67
|
+
"""
|
|
68
|
+
|
|
69
|
+
name: str
|
|
70
|
+
note: str
|
|
71
|
+
mode: str
|
|
72
|
+
accent: str
|
|
73
|
+
on_accent: str
|
|
74
|
+
accent_dim: str
|
|
75
|
+
on_accent_dim: str
|
|
76
|
+
surface: str
|
|
77
|
+
surface_lowest: str
|
|
78
|
+
surface_low: str
|
|
79
|
+
surface_container: str
|
|
80
|
+
surface_high: str
|
|
81
|
+
surface_highest: str
|
|
82
|
+
on_surface: str
|
|
83
|
+
on_surface_variant: str
|
|
84
|
+
outline: str
|
|
85
|
+
outline_variant: str
|
|
86
|
+
data: str
|
|
87
|
+
model: str
|
|
88
|
+
highlight: str
|
|
89
|
+
ink: str
|
|
90
|
+
grid: str
|
|
91
|
+
|
|
92
|
+
|
|
93
|
+
THEMES = {
|
|
94
|
+
"instrument": Theme(
|
|
95
|
+
name="instrument",
|
|
96
|
+
note="Near-black teal-tinted surfaces, one electric accent.",
|
|
97
|
+
mode="dark",
|
|
98
|
+
accent="#2BE4C8",
|
|
99
|
+
on_accent="#04211C",
|
|
100
|
+
accent_dim="#123B33",
|
|
101
|
+
on_accent_dim="#7FEFD9",
|
|
102
|
+
surface="#0B0F10",
|
|
103
|
+
surface_lowest="#080C0D",
|
|
104
|
+
surface_low="#11181A",
|
|
105
|
+
surface_container="#141C1E",
|
|
106
|
+
surface_high="#182123",
|
|
107
|
+
surface_highest="#1C2628",
|
|
108
|
+
on_surface="#E4EBEA",
|
|
109
|
+
on_surface_variant="#8CA0A0",
|
|
110
|
+
outline="#3C4C4E",
|
|
111
|
+
outline_variant="#233032",
|
|
112
|
+
data="#2BE4C8",
|
|
113
|
+
model="#FF4D9E",
|
|
114
|
+
highlight="#7FEFD9",
|
|
115
|
+
ink="#D9E2E0",
|
|
116
|
+
grid="#3A4649",
|
|
117
|
+
),
|
|
118
|
+
"neon_violet": Theme(
|
|
119
|
+
name="neon_violet",
|
|
120
|
+
mode="dark",
|
|
121
|
+
note="Violet chrome with hot-pink data on black.",
|
|
122
|
+
accent="#9929EA",
|
|
123
|
+
on_accent="#FFFFFF",
|
|
124
|
+
accent_dim="#2E1147",
|
|
125
|
+
on_accent_dim="#D9A6FF",
|
|
126
|
+
surface="#000000",
|
|
127
|
+
surface_lowest="#0A0410",
|
|
128
|
+
surface_low="#120818",
|
|
129
|
+
surface_container="#170A22",
|
|
130
|
+
surface_high="#1E0E2C",
|
|
131
|
+
surface_highest="#251236",
|
|
132
|
+
on_surface="#F3EAFB",
|
|
133
|
+
on_surface_variant="#A98BC4",
|
|
134
|
+
outline="#3B1B57",
|
|
135
|
+
outline_variant="#26113A",
|
|
136
|
+
data="#FF5FCF",
|
|
137
|
+
model="#FAEB92",
|
|
138
|
+
highlight="#9929EA",
|
|
139
|
+
ink="#F3EAFB",
|
|
140
|
+
grid="#3B1B57",
|
|
141
|
+
),
|
|
142
|
+
"neon_gold": Theme(
|
|
143
|
+
name="neon_gold",
|
|
144
|
+
mode="dark",
|
|
145
|
+
note="The violet palette with pale gold as the accent instead.",
|
|
146
|
+
accent="#FAEB92",
|
|
147
|
+
on_accent="#1A0B26",
|
|
148
|
+
accent_dim="#3E3418",
|
|
149
|
+
on_accent_dim="#FAEB92",
|
|
150
|
+
surface="#000000",
|
|
151
|
+
surface_lowest="#0C0610",
|
|
152
|
+
surface_low="#150823",
|
|
153
|
+
surface_container="#1A0B26",
|
|
154
|
+
surface_high="#221030",
|
|
155
|
+
surface_highest="#29143A",
|
|
156
|
+
on_surface="#F6EEFC",
|
|
157
|
+
on_surface_variant="#AE90C8",
|
|
158
|
+
outline="#43205F",
|
|
159
|
+
outline_variant="#2B1440",
|
|
160
|
+
data="#FF5FCF",
|
|
161
|
+
model="#9929EA",
|
|
162
|
+
highlight="#FAEB92",
|
|
163
|
+
ink="#F6EEFC",
|
|
164
|
+
grid="#43205F",
|
|
165
|
+
),
|
|
166
|
+
"retro_green": Theme(
|
|
167
|
+
name="retro_green",
|
|
168
|
+
mode="dark",
|
|
169
|
+
note="Matrix mood with a calmer green and a legible olive card.",
|
|
170
|
+
accent="#08CB00",
|
|
171
|
+
on_accent="#00230A",
|
|
172
|
+
accent_dim="#12400A",
|
|
173
|
+
on_accent_dim="#7CFF77",
|
|
174
|
+
surface="#000000",
|
|
175
|
+
surface_lowest="#050805",
|
|
176
|
+
surface_low="#1B2A00",
|
|
177
|
+
surface_container="#253900",
|
|
178
|
+
surface_high="#2E4600",
|
|
179
|
+
surface_highest="#375300",
|
|
180
|
+
on_surface="#EEEEEE",
|
|
181
|
+
on_surface_variant="#8FA07A",
|
|
182
|
+
outline="#3A5410",
|
|
183
|
+
outline_variant="#223300",
|
|
184
|
+
data="#08CB00",
|
|
185
|
+
model="#EEEEEE",
|
|
186
|
+
highlight="#7CFF77",
|
|
187
|
+
ink="#EEEEEE",
|
|
188
|
+
grid="#3A5410",
|
|
189
|
+
),
|
|
190
|
+
"paper": Theme(
|
|
191
|
+
name="paper",
|
|
192
|
+
note="Warm paper surfaces, graphite ink and a vermilion accent.",
|
|
193
|
+
mode="light",
|
|
194
|
+
accent="#C14B2E",
|
|
195
|
+
on_accent="#FFF6F1",
|
|
196
|
+
accent_dim="#F2D9CE",
|
|
197
|
+
on_accent_dim="#8A2F1B",
|
|
198
|
+
surface="#FAF6EF",
|
|
199
|
+
surface_lowest="#F1EAD9",
|
|
200
|
+
surface_low="#FFFFFF",
|
|
201
|
+
surface_container="#FFFDF8",
|
|
202
|
+
surface_high="#FFFFFF",
|
|
203
|
+
surface_highest="#FFFFFF",
|
|
204
|
+
on_surface="#33302A",
|
|
205
|
+
on_surface_variant="#7A7264",
|
|
206
|
+
outline="#C9BFA9",
|
|
207
|
+
outline_variant="#E5DECC",
|
|
208
|
+
data="#C14B2E",
|
|
209
|
+
model="#2E6F8E",
|
|
210
|
+
highlight="#E0A458",
|
|
211
|
+
ink="#33302A",
|
|
212
|
+
grid="#C9BFA9",
|
|
213
|
+
),
|
|
214
|
+
"mint": Theme(
|
|
215
|
+
name="mint",
|
|
216
|
+
note="Light mint greens on white, colorhunt's fresh-green family.",
|
|
217
|
+
mode="light",
|
|
218
|
+
accent="#3E8E5A",
|
|
219
|
+
on_accent="#F2FBF4",
|
|
220
|
+
accent_dim="#D8F0DC",
|
|
221
|
+
on_accent_dim="#2C6E44",
|
|
222
|
+
surface="#F4FAEF",
|
|
223
|
+
surface_lowest="#E4F3DA",
|
|
224
|
+
surface_low="#FFFFFF",
|
|
225
|
+
surface_container="#FBFEF8",
|
|
226
|
+
surface_high="#FFFFFF",
|
|
227
|
+
surface_highest="#FFFFFF",
|
|
228
|
+
on_surface="#24312A",
|
|
229
|
+
on_surface_variant="#6D8072",
|
|
230
|
+
outline="#B9CDB9",
|
|
231
|
+
outline_variant="#DCE9D8",
|
|
232
|
+
data="#3E8E5A",
|
|
233
|
+
model="#B85B9E",
|
|
234
|
+
highlight="#7FBF6C",
|
|
235
|
+
ink="#24312A",
|
|
236
|
+
grid="#B9CDB9",
|
|
237
|
+
),
|
|
238
|
+
"glacier": Theme(
|
|
239
|
+
name="glacier",
|
|
240
|
+
note="Pale glacier blues with a deep slate accent.",
|
|
241
|
+
mode="light",
|
|
242
|
+
accent="#4A628A",
|
|
243
|
+
on_accent="#F2F7FF",
|
|
244
|
+
accent_dim="#D5E4EE",
|
|
245
|
+
on_accent_dim="#35577D",
|
|
246
|
+
surface="#F2F8FA",
|
|
247
|
+
surface_lowest="#E0EEF2",
|
|
248
|
+
surface_low="#FFFFFF",
|
|
249
|
+
surface_container="#FAFDFE",
|
|
250
|
+
surface_high="#FFFFFF",
|
|
251
|
+
surface_highest="#FFFFFF",
|
|
252
|
+
on_surface="#22303B",
|
|
253
|
+
on_surface_variant="#64798A",
|
|
254
|
+
outline="#B4C8D4",
|
|
255
|
+
outline_variant="#DCE8EE",
|
|
256
|
+
data="#3D7EA6",
|
|
257
|
+
model="#C96A4A",
|
|
258
|
+
highlight="#4A628A",
|
|
259
|
+
ink="#22303B",
|
|
260
|
+
grid="#B4C8D4",
|
|
261
|
+
),
|
|
262
|
+
"lavender": Theme(
|
|
263
|
+
name="lavender",
|
|
264
|
+
note="Soft lavender neutrals with a deep violet accent.",
|
|
265
|
+
mode="light",
|
|
266
|
+
accent="#7C6BD6",
|
|
267
|
+
on_accent="#F8F6FF",
|
|
268
|
+
accent_dim="#E3DBF7",
|
|
269
|
+
on_accent_dim="#5A4BB0",
|
|
270
|
+
surface="#F7F4FC",
|
|
271
|
+
surface_lowest="#EDE6F7",
|
|
272
|
+
surface_low="#FFFFFF",
|
|
273
|
+
surface_container="#FCFAFF",
|
|
274
|
+
surface_high="#FFFFFF",
|
|
275
|
+
surface_highest="#FFFFFF",
|
|
276
|
+
on_surface="#2C2838",
|
|
277
|
+
on_surface_variant="#756E88",
|
|
278
|
+
outline="#C4BBDA",
|
|
279
|
+
outline_variant="#E4DEF1",
|
|
280
|
+
data="#7C6BD6",
|
|
281
|
+
model="#D66B8F",
|
|
282
|
+
highlight="#A594F9",
|
|
283
|
+
ink="#2C2838",
|
|
284
|
+
grid="#C4BBDA",
|
|
285
|
+
),
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
DEFAULT = "paper"
|
|
289
|
+
|
|
290
|
+
# The process-wide selection, rebound only by use() before the app serves. This
|
|
291
|
+
# is the one piece of module state the per-session rule tolerates: Flet calls
|
|
292
|
+
# main(page) per browser connection, but every session resolves the same theme,
|
|
293
|
+
# so it is config rather than shared mutable state.
|
|
294
|
+
ACTIVE = THEMES[DEFAULT]
|
|
295
|
+
|
|
296
|
+
|
|
297
|
+
def names():
|
|
298
|
+
"""The registered themes as {name: note}, in registration order."""
|
|
299
|
+
return {name: theme.note for name, theme in THEMES.items()}
|
|
300
|
+
|
|
301
|
+
|
|
302
|
+
def use(name):
|
|
303
|
+
"""
|
|
304
|
+
Select the process-wide theme by name and return it.
|
|
305
|
+
|
|
306
|
+
Called once from shell.build_main before the app serves; see ACTIVE on why
|
|
307
|
+
process-wide state is safe here.
|
|
308
|
+
"""
|
|
309
|
+
global ACTIVE
|
|
310
|
+
if name not in THEMES:
|
|
311
|
+
raise ValueError(f"theme must be one of {sorted(THEMES)}, got {name!r}.")
|
|
312
|
+
ACTIVE = THEMES[name]
|
|
313
|
+
return ACTIVE
|
|
314
|
+
|
|
315
|
+
|
|
316
|
+
def active():
|
|
317
|
+
"""
|
|
318
|
+
The active Theme: the palette figures and equations render with.
|
|
319
|
+
|
|
320
|
+
Read at draw time, never bound at import, so a figure follows whichever
|
|
321
|
+
theme open(theme=...) selected.
|
|
322
|
+
"""
|
|
323
|
+
return ACTIVE
|