ngsolve-gui 0.0.1__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.
@@ -0,0 +1,3 @@
1
+ __version__ = "0.0.1"
2
+
3
+ from .app import NGSolveGui
@@ -0,0 +1,4 @@
1
+ from .run import main
2
+
3
+ if __name__ == "__main__":
4
+ main()
ngsolve_gui/app.py ADDED
@@ -0,0 +1,514 @@
1
+ import ctypes
2
+ import os
3
+ import threading
4
+ import time
5
+
6
+ from ngapp.app import App
7
+ from ngapp.components import *
8
+
9
+ from .app_data import AppData
10
+ from .file_loader import load_file
11
+ from ngapp.keybindings import KeybindingManager, keybinding_styles
12
+ from .navigator import Navigator
13
+ from .property_panel import PropertyPanel
14
+ from .styles import css, theme, flex_fill, panel_full
15
+ from .system_monitor import SystemMonitor
16
+
17
+
18
+ class Panel(Div):
19
+ def __init__(self, app_data):
20
+ self.app_data = app_data
21
+ self.comp = None
22
+ super().__init__(ui_class=str(panel_full))
23
+ self.set_tab()
24
+
25
+ def set_tab(self):
26
+ name = self.app_data.active_tab
27
+ if name is None:
28
+ self.ui_children = []
29
+ return
30
+ tab = self.app_data.get_tab(name)
31
+ if tab is None:
32
+ self.ui_children = []
33
+ return
34
+ if "component" in tab:
35
+ self.comp = comp = tab["component"]
36
+ else:
37
+ cls = self._resolve_class(tab["type"])
38
+ self.comp = comp = cls(
39
+ tab["name"],
40
+ tab["data"],
41
+ app_data=self.app_data,
42
+ )
43
+ tab["component"] = comp
44
+ self.ui_children = [comp]
45
+
46
+ def _resolve_class(self, type_key):
47
+ from .registry import get_component_info
48
+
49
+ info = get_component_info(type_key)
50
+ if info is None:
51
+ raise ValueError(f"Unknown component type: {type_key}")
52
+ return info["cls"]
53
+
54
+
55
+ class Settings(QMenu):
56
+ def __init__(self, app):
57
+ self.app = app
58
+ val = self.app.usersettings.get("nthreads", 0)
59
+ nthreads = QInput(
60
+ QTooltip(
61
+ "Set number of threads used by NGSolve, 0 for all available cores. Only takes effect after restarting the application."
62
+ ),
63
+ ui_label="Number of Threads",
64
+ ui_type="number",
65
+ ui_model_value=val,
66
+ )
67
+ nthreads.on_update_model_value(self.app.usersettings.update("nthreads"))
68
+
69
+ show_axes = QCheckbox(
70
+ ui_label="Show Axes by Default",
71
+ ui_model_value=self.app.usersettings.get("axes_visible", True),
72
+ )
73
+ show_axes.on_update_model_value(self.app.usersettings.update("axes_visible"))
74
+
75
+ show_navcube = QCheckbox(
76
+ ui_label="Show Navigation Cube by Default",
77
+ ui_model_value=self.app.usersettings.get("navcube_visible", False),
78
+ )
79
+ show_navcube.on_update_model_value(self.app.usersettings.update("navcube_visible"))
80
+
81
+ scale_by_mag = QCheckbox(
82
+ ui_label="Scale Vectors by Magnitude by Default",
83
+ ui_model_value=self.app.usersettings.get("scale_by_magnitude", True),
84
+ )
85
+ scale_by_mag.on_update_model_value(self.app.usersettings.update("scale_by_magnitude"))
86
+
87
+ super().__init__(QCard(
88
+ QCardSection("Settings"),
89
+ QCardSection(nthreads, show_axes, show_navcube, scale_by_mag),
90
+ ))
91
+
92
+
93
+ class StatusBar(Div):
94
+ """Floating pill overlay at the bottom of the scene showing loading progress."""
95
+
96
+ # Outer wrapper — anchored to the bottom-center of the nearest
97
+ # position:relative ancestor (the scene container).
98
+ _VISIBLE = (
99
+ "position: fixed; bottom: 24px; left: 50%; transform: translateX(-50%); "
100
+ "z-index: 1000; display: flex; flex-direction: column; align-items: stretch; "
101
+ "background: rgba(15,23,42,0.88); backdrop-filter: blur(6px); "
102
+ "border-radius: 12px; padding: 10px 18px 12px; min-width: 320px; "
103
+ "max-width: 480px; box-shadow: 0 4px 24px rgba(0,0,0,0.25); "
104
+ "color: #e2e8f0; font-size: 0.82rem;"
105
+ )
106
+ _HIDDEN = "display: none;"
107
+
108
+ def __init__(self):
109
+ # Top row: icon + label + cancel
110
+ self._label = Div(
111
+ "",
112
+ ui_style="white-space: nowrap; overflow: hidden; text-overflow: ellipsis;",
113
+ )
114
+ self._pct_label = Div(
115
+ "",
116
+ ui_style=(
117
+ "white-space: nowrap; font-variant-numeric: tabular-nums; "
118
+ "font-size: 0.78rem; color: #94a3b8; min-width: 36px; text-align: right;"
119
+ ),
120
+ )
121
+ self._cancel_btn = QBtn(
122
+ QTooltip("Cancel"),
123
+ ui_icon="mdi-close",
124
+ ui_flat=True,
125
+ ui_dense=True,
126
+ ui_round=True,
127
+ ui_size="xs",
128
+ ui_padding="2px",
129
+ ui_color="grey-5",
130
+ )
131
+ self._cancel_btn.on_click(self._on_cancel)
132
+
133
+ top_row = Div(
134
+ QSpinner(ui_color="accent", ui_size="18px"),
135
+ self._label,
136
+ QSpace(),
137
+ self._pct_label,
138
+ self._cancel_btn,
139
+ ui_style="display: flex; align-items: center; gap: 8px;",
140
+ )
141
+
142
+ # Progress bar (track + filled portion)
143
+ self._bar_fill = Div(
144
+ ui_style=(
145
+ "height: 100%; width: 0%; border-radius: 3px; "
146
+ "background: linear-gradient(90deg, #14B8A6, #0EA5E9); "
147
+ "transition: width 0.3s ease;"
148
+ ),
149
+ )
150
+ bar_track = Div(
151
+ self._bar_fill,
152
+ ui_style=(
153
+ "height: 6px; border-radius: 3px; "
154
+ "background: rgba(255,255,255,0.12); margin-top: 8px; "
155
+ "overflow: hidden;"
156
+ ),
157
+ )
158
+
159
+ self._thread = None
160
+ self._done_event = None
161
+ self._generation = 0
162
+
163
+ super().__init__(top_row, bar_track, ui_style=self._HIDDEN)
164
+
165
+ def show(self, filename, thread, done_event):
166
+ self._generation += 1
167
+ self._thread = thread
168
+ self._done_event = done_event
169
+ self._thread_name = thread.name if thread else ""
170
+ self._label.ui_children = [f"Running {filename} \u2026"]
171
+ self._pct_label.ui_children = [""]
172
+ self._bar_fill.ui_style = (
173
+ "height: 100%; width: 100%; border-radius: 3px; "
174
+ "background: linear-gradient(90deg, #14B8A6, #0EA5E9); "
175
+ "animation: indeterminate 1.4s ease infinite; "
176
+ "transition: none;"
177
+ )
178
+ self.ui_style = self._VISIBLE
179
+ self._start_poll(self._generation)
180
+
181
+ def hide(self):
182
+ self._thread = None
183
+ self._done_event = None
184
+ self.ui_style = self._HIDDEN
185
+
186
+ def _set_progress(self, percent):
187
+ """Set determinate progress (0–100)."""
188
+ w = max(0, min(100, percent))
189
+ self._bar_fill.ui_style = (
190
+ f"height: 100%; width: {w:.1f}%; border-radius: 3px; "
191
+ "background: linear-gradient(90deg, #14B8A6, #0EA5E9); "
192
+ "transition: width 0.3s ease;"
193
+ )
194
+ self._pct_label.ui_children = [f"{w:.0f}%"]
195
+
196
+ def _set_indeterminate(self):
197
+ self._bar_fill.ui_style = (
198
+ "height: 100%; width: 100%; border-radius: 3px; "
199
+ "background: linear-gradient(90deg, #14B8A6, #0EA5E9); "
200
+ "animation: indeterminate 1.4s ease infinite; "
201
+ "transition: none;"
202
+ )
203
+ self._pct_label.ui_children = [""]
204
+
205
+ def _start_poll(self, gen):
206
+ def poll():
207
+ from netgen.libngpy._meshing import _GetStatus
208
+
209
+ while gen == self._generation:
210
+ time.sleep(0.3)
211
+ done_event = self._done_event
212
+ if done_event is None:
213
+ break
214
+ try:
215
+ status_text, percent = _GetStatus()
216
+ except Exception:
217
+ status_text, percent = "idle", 0.0
218
+
219
+ done = done_event.is_set()
220
+
221
+ if status_text and status_text != "idle":
222
+ self._label.ui_children = [status_text]
223
+ if percent > 0:
224
+ self._set_progress(percent)
225
+ else:
226
+ self._set_indeterminate()
227
+ # Script finished but netgen hasn't reset status yet
228
+ if done:
229
+ self.hide()
230
+ break
231
+ elif done:
232
+ self.hide()
233
+ break
234
+
235
+ threading.Thread(target=poll, daemon=True, name="StatusPoll").start()
236
+
237
+ def _on_cancel(self):
238
+ self._generation += 1
239
+ thread = self._thread
240
+ # Only interrupt non-IPython threads; the IPython shell stays
241
+ # alive for interactive use — just dismiss the pill.
242
+ if (
243
+ thread
244
+ and thread.is_alive()
245
+ and self._thread_name != "IPythonEmbedder"
246
+ ):
247
+ try:
248
+ ctypes.pythonapi.PyThreadState_SetAsyncExc(
249
+ ctypes.c_ulong(thread.ident),
250
+ ctypes.py_object(KeyboardInterrupt),
251
+ )
252
+ except Exception:
253
+ pass
254
+ self.hide()
255
+
256
+
257
+ class NGSolveGui(App):
258
+ def __init__(self, filename=None, local_path=None):
259
+ self._local_path = local_path if local_path else os.path.expanduser("~")
260
+ self.app_data = AppData()
261
+
262
+ # Toolbar buttons
263
+ upload_file = QBtn(QTooltip("Load File"), ui_flat=True, ui_icon="mdi-plus")
264
+ upload_file.on_click(self._load_file)
265
+ savebtn = QBtn(
266
+ QTooltip("Save Project"), ui_flat=True, ui_icon="mdi-content-save"
267
+ )
268
+ savebtn.on_click(self.save_local)
269
+ loadbtn = QBtn(
270
+ QTooltip("Load Project"), ui_flat=True, ui_icon="mdi-folder-open"
271
+ )
272
+ loadbtn.on_click(self.load_local)
273
+
274
+ # Panel toggle buttons
275
+ self._nav_btn = QBtn(
276
+ QTooltip("Toggle Navigator"),
277
+ ui_flat=True,
278
+ ui_icon="mdi-page-layout-sidebar-left",
279
+ )
280
+ self._nav_btn.on_click(self._toggle_navigator)
281
+ self._prop_btn = QBtn(
282
+ QTooltip("Toggle Properties"),
283
+ ui_flat=True,
284
+ ui_icon="mdi-page-layout-sidebar-right",
285
+ )
286
+ self._prop_btn.on_click(self._toggle_property_panel)
287
+
288
+ settings_btn = QBtn(
289
+ Settings(self), QTooltip("User Settings"), ui_flat=True, ui_icon="mdi-cog"
290
+ )
291
+ close_btn = QBtn(QTooltip("Quit"), ui_flat=True, ui_icon="mdi-close")
292
+ close_btn.on_click(self.quit)
293
+ ngs_logo = Div(
294
+ QImg(
295
+ ui_src=self.load_asset("logo_withname_retina.png"),
296
+ ui_height="40px",
297
+ ui_fit="scale-down",
298
+ ),
299
+ ui_style="width: 200px;",
300
+ )
301
+
302
+ self.system_monitor = SystemMonitor()
303
+
304
+ bar = QBar(
305
+ ngs_logo,
306
+ upload_file,
307
+ savebtn,
308
+ loadbtn,
309
+ QSpace(),
310
+ self.system_monitor,
311
+ self._nav_btn,
312
+ self._prop_btn,
313
+ settings_btn,
314
+ close_btn,
315
+ ui_style="height: 60px",
316
+ ui_class="bg-primary text-grey-4",
317
+ )
318
+
319
+ # Three-column layout using flex
320
+ self.navigator = Navigator(self.app_data, self._click_tab)
321
+ self.property_panel = PropertyPanel()
322
+ self.tab_panel = Panel(self.app_data)
323
+ self.status_bar = StatusBar()
324
+
325
+ self._nav_visible = self.usersettings.get("nav_visible", True)
326
+ self._prop_visible = self.usersettings.get("prop_visible", True)
327
+ self._nav_width = self.usersettings.get("nav_width", 200)
328
+ self._prop_width = self.usersettings.get("prop_width", 280)
329
+
330
+ self.kb = KeybindingManager(self, theme=theme)
331
+
332
+ # Inner splitter: center | property panel (reverse so model = prop width)
333
+ self._inner_splitter = QSplitter(
334
+ ui_model_value=self._prop_width if self._prop_visible else 0,
335
+ ui_unit="px",
336
+ ui_reverse=True,
337
+ ui_limits=[0, 500] if self._prop_visible else [0, 0],
338
+ ui_emit_immediately=True,
339
+ ui_slots={
340
+ "before": [Div(self.tab_panel, ui_class=str(flex_fill))],
341
+ "after": [self.property_panel],
342
+ },
343
+ ui_style="height: calc(100vh - 60px);",
344
+ )
345
+ self._inner_splitter.on_update_model_value(self._on_prop_width_change)
346
+
347
+ # Outer splitter: navigator | inner splitter
348
+ self._outer_splitter = QSplitter(
349
+ ui_model_value=self._nav_width if self._nav_visible else 0,
350
+ ui_unit="px",
351
+ ui_limits=[0, 500] if self._nav_visible else [0, 0],
352
+ ui_emit_immediately=True,
353
+ ui_slots={
354
+ "before": [self.navigator],
355
+ "after": [self._inner_splitter],
356
+ },
357
+ ui_style="height: calc(100vh - 60px);",
358
+ )
359
+ self._outer_splitter.on_update_model_value(self._on_nav_width_change)
360
+
361
+ page = self._outer_splitter
362
+
363
+ super().__init__(bar, page, self.status_bar, self.kb.indicator, self.kb.help_overlay)
364
+
365
+ theme.apply(self)
366
+ css.inject(self)
367
+ keybinding_styles.inject(self)
368
+ self._inject_status_bar_css()
369
+ self.on_load(self.__on_load)
370
+
371
+ # -- Global keybindings (always active) --
372
+ kb = self.kb
373
+ kb.add("h", kb.toggle_help, "Show keyboard shortcuts", "General")
374
+ kb.add("ctrl+b", self._toggle_navigator, "Toggle navigator", "Panels")
375
+ kb.add(
376
+ "ctrl+alt+b", self._toggle_property_panel, "Toggle property panel", "Panels"
377
+ )
378
+ for i in range(1, 10):
379
+ kb.add(
380
+ str(i),
381
+ lambda n=i: self.navigator.select_by_index(n),
382
+ f"Select item {i}",
383
+ "Navigation",
384
+ )
385
+ self.add_keybinding("escape", lambda e: self.kb.on_escape())
386
+ self.on_before_save(self.__on_before_save)
387
+ if isinstance(filename, str):
388
+ self._load_with_status(filename)
389
+ elif isinstance(filename, list):
390
+ for f in filename:
391
+ self._load_with_status(f)
392
+
393
+ def _inject_status_bar_css(self):
394
+ kf = (
395
+ "@keyframes indeterminate {"
396
+ " 0% { transform: translateX(-100%); }"
397
+ " 100% { transform: translateX(100%); }"
398
+ "}"
399
+ )
400
+
401
+ def _inject(js):
402
+ el = js.document.createElement("style")
403
+ el.textContent = kf
404
+ js.document.head.appendChild(el)
405
+
406
+ self.call_js(_inject)
407
+
408
+ def _load_file(self):
409
+ from tkinter import Tk, filedialog
410
+
411
+ root = Tk()
412
+ root.withdraw()
413
+ file_path = filedialog.askopenfilename(
414
+ title="Select a file",
415
+ initialdir=self._local_path,
416
+ filetypes=[
417
+ ("All Files", "*.*"),
418
+ ("Mesh Files", "*.vol *.vol.gz"),
419
+ ("Geometry Files", "*.step *.iges *.stp"),
420
+ ],
421
+ )
422
+ root.destroy()
423
+ if file_path:
424
+ self._local_path = os.path.dirname(file_path)
425
+ self._load_with_status(file_path)
426
+
427
+ def _load_with_status(self, filename):
428
+ if not filename:
429
+ return
430
+ result = load_file(filename, self)
431
+ if result:
432
+ thread, done_event = result
433
+ name = os.path.basename(str(filename))
434
+ self.status_bar.show(name, thread, done_event)
435
+
436
+ def __on_before_save(self):
437
+ self.storage.set("app_data", self.app_data.get_save_data(), use_pickle=True)
438
+
439
+ def __on_load(self):
440
+ data = self.storage.get("app_data")
441
+ if data is not None:
442
+ self.app_data._data.update(data)
443
+ self._update()
444
+ self.app_data._update = self._update
445
+
446
+ def _click_tab(self, tabname):
447
+ self.app_data.active_tab = tabname
448
+ self.tab_panel.set_tab()
449
+ self.navigator.update()
450
+ comp = self.tab_panel.comp
451
+ tab = self.app_data.get_tab(tabname)
452
+ type_key = tab.get("type", "") if tab else ""
453
+ self.property_panel.set_component(comp, type_key)
454
+ self.kb.set_component(comp)
455
+
456
+ def _toggle_navigator(self):
457
+ self._nav_visible = not self._nav_visible
458
+ self.usersettings.set("nav_visible", self._nav_visible)
459
+ self._apply_panel_visibility()
460
+
461
+ def _toggle_property_panel(self):
462
+ self._prop_visible = not self._prop_visible
463
+ self.usersettings.set("prop_visible", self._prop_visible)
464
+ self._apply_panel_visibility()
465
+
466
+ def _on_nav_width_change(self, event):
467
+ val = int(event.value)
468
+ if val > 0:
469
+ self._nav_width = val
470
+ self.usersettings.set("nav_width", val)
471
+
472
+ def _on_prop_width_change(self, event):
473
+ val = int(event.value)
474
+ if val > 0:
475
+ self._prop_width = val
476
+ self.usersettings.set("prop_width", val)
477
+
478
+ def _apply_panel_visibility(self):
479
+ if not hasattr(self, "_outer_splitter"):
480
+ return
481
+ if self._nav_visible:
482
+ self._outer_splitter.ui_model_value = self._nav_width
483
+ self._outer_splitter.ui_limits = [0, 500]
484
+ else:
485
+ self._outer_splitter.ui_model_value = 0
486
+ self._outer_splitter.ui_limits = [0, 0]
487
+ if self._prop_visible:
488
+ self._inner_splitter.ui_model_value = self._prop_width
489
+ self._inner_splitter.ui_limits = [0, 500]
490
+ else:
491
+ self._inner_splitter.ui_model_value = 0
492
+ self._inner_splitter.ui_limits = [0, 0]
493
+
494
+
495
+ def redraw(self, *args, **kwargs):
496
+ self.app_data.set_needs_redraw()
497
+ comp = self.tab_panel.comp
498
+ if comp is not None:
499
+ if hasattr(comp, "redraw"):
500
+ comp.redraw()
501
+
502
+ def _update(self):
503
+ self.navigator.update()
504
+ self.tab_panel.set_tab()
505
+ active = self.app_data.active_tab
506
+ if active:
507
+ comp = self.tab_panel.comp
508
+ tab = self.app_data.get_tab(active)
509
+ type_key = tab.get("type", "") if tab else ""
510
+ self.property_panel.set_component(comp, type_key)
511
+ self.kb.set_component(comp)
512
+ else:
513
+ self.property_panel.set_component(None, "")
514
+ self.kb.set_component(None)
@@ -0,0 +1,134 @@
1
+ from ngsolve_webgpu import *
2
+ from webgpu.camera import Camera
3
+
4
+
5
+ class AppData:
6
+ _data: dict
7
+ _gpu_cache: dict
8
+ _clipping: Clipping
9
+ _camera: Camera
10
+
11
+ def __init__(self):
12
+ self._data = {"tabs": {}, "active_tab": None}
13
+ self._update = None
14
+ self._gpu_cache = {}
15
+ self._clipping = Clipping()
16
+ self._camera = Camera()
17
+
18
+ @property
19
+ def clipping(self):
20
+ return self._clipping
21
+
22
+ @property
23
+ def camera(self):
24
+ return self._camera
25
+
26
+ def get_mesh_gpu_data(self, mesh):
27
+ key = repr(mesh)
28
+ if key not in self._gpu_cache:
29
+ self._gpu_cache[key] = MeshData(mesh)
30
+ return self._gpu_cache[key]
31
+
32
+ def get_function_gpu_data(self, cf, mesh, **kwargs):
33
+ key = hash((repr(cf), repr(mesh), tuple(sorted(kwargs.items()))))
34
+ if key not in self._gpu_cache:
35
+ mdata = self.get_mesh_gpu_data(mesh)
36
+ self._gpu_cache[key] = FunctionData(mdata, cf, **kwargs)
37
+ return self._gpu_cache[key]
38
+
39
+ def set_needs_redraw(self):
40
+ for tab in self._data["tabs"].values():
41
+ if "component" in tab:
42
+ tab["component"]._redraw_needed = True
43
+
44
+ def get_save_data(self):
45
+ from ngapp.observable import snapshot
46
+
47
+ data_copy = {"tabs": {}, "active_tab": self._data["active_tab"]}
48
+ for name, tab in self._data["tabs"].items():
49
+ tab_copy = tab.copy()
50
+ if "component" in tab_copy:
51
+ comp = tab_copy["component"]
52
+ # Resolve type key from registry if not already set
53
+ if "type" not in tab_copy or tab_copy["type"] == "unknown":
54
+ from .registry import get_registry
55
+
56
+ cls = type(comp)
57
+ for key, info in get_registry().items():
58
+ if info["cls"] is cls:
59
+ tab_copy["type"] = key
60
+ break
61
+ tab_copy["data"] = comp.data
62
+ tab_copy["settings"] = snapshot(comp)
63
+ del tab_copy["component"]
64
+ data_copy["tabs"][name] = tab_copy
65
+ return data_copy
66
+
67
+ def add_tab(self, title: str, cls: type, *args, **kwargs):
68
+ name = title.lower().replace(" ", "_")
69
+ # Resolve type key and icon from registry
70
+ from .registry import get_registry
71
+
72
+ type_key = "unknown"
73
+ icon = "mdi-vector-triangle"
74
+ for key, info in get_registry().items():
75
+ if info["cls"] is cls:
76
+ type_key = key
77
+ icon = info["icon"]
78
+ break
79
+ self._data["tabs"][name] = {
80
+ "type": type_key,
81
+ "icon": icon,
82
+ "data": {},
83
+ "name": name,
84
+ "title": title,
85
+ "settings": {},
86
+ }
87
+ component = cls(name, *args, **kwargs)
88
+ self._data["tabs"][name]["component"] = component
89
+ self.active_tab = name
90
+ if self._update is not None:
91
+ self._update()
92
+ return component
93
+
94
+ def get_tabs(self):
95
+ """
96
+ Get the tabs stored in the AppData instance.
97
+
98
+ :return: A dictionary of tabs.
99
+ """
100
+ return self._data["tabs"]
101
+
102
+ def get_tab(self, name):
103
+ return self._data["tabs"].get(name, None)
104
+
105
+ def delete_tab(self, name):
106
+ """
107
+ Delete a tab by name.
108
+
109
+ :param name: The name of the tab to delete.
110
+ """
111
+ if name in self._data["tabs"]:
112
+ del self._data["tabs"][name]
113
+ if self.active_tab == name:
114
+ self.active_tab = (
115
+ list(self._data["tabs"].keys())[0] if self._data["tabs"] else None
116
+ )
117
+ if self._update is not None:
118
+ self._update()
119
+
120
+ @property
121
+ def active_tab(self):
122
+ """
123
+ Get the currently active tab name.
124
+ """
125
+ return self._data["active_tab"]
126
+
127
+ @active_tab.setter
128
+ def active_tab(self, name):
129
+ """
130
+ Set the currently active tab by name.
131
+
132
+ :param name: The name of the tab to set as active.
133
+ """
134
+ self._data["active_tab"] = name
@@ -0,0 +1,14 @@
1
+ from ngapp import AppConfig
2
+
3
+ from . import __version__
4
+ from .app import NGSolveGui
5
+
6
+ _DESCRIPTION = """A short description"""
7
+
8
+ config = AppConfig(
9
+ name="NGSolve GUI",
10
+ version=__version__,
11
+ python_class=NGSolveGui,
12
+ frontend_pip_dependencies=["ngsolve", "ngsolve_webgpu"],
13
+ description=_DESCRIPTION,
14
+ )
@@ -0,0 +1,3 @@
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:7e14fa7628c46434a707b14d3580c2c11d8b779386c7a181681c80ae42f1b8a2
3
+ size 70822