UI Shell Architecture
8.1 Viewer Loop
ViewerApplication(ui/shell/src/runner.rs) owns the winit event loop: under winit 0.30 it implementswinit::application::ApplicationHandler(resumed/window_event/device_event/about_to_wait) and is driven byevent_loop.run_app(&mut app).ViewerLoop(ui/shell/src/viewer_loop/runtime/state.rs, re-exported viamod.rs) owns the renderer, egui context, camera controller, and complete viewer state.ViewerApplicationforwards eachApplicationHandlercallback toViewerLoop’s matching method (window_event/device_event/about_to_wait), which translate winit events into UI interactions.- The event loop is decomposed into specialised modules:
key_actions/dispatches keyboard shortcuts viaKeyActionContext.menu_actions/mirrors the GUI menu tree (themenu/module) and triggers async loads, toggles, and exports.progress_overlay.rs,fragment_palette.rs,change_element/,smiles_builder_dialog.rs,conformer_dialog.rs, andresidue_mutation_dialog.rs,residue_protonation_dialog.rs,terminal_cap_dialog.rs, anddisulfide_dialog.rsisolate complex overlays/dialogs for readability.dialog_context.rsprovides a typed wrapper around egui state and viewer handles to simplify passing dependencies into modal dialogs.
- When introducing new interaction families, prefer a dedicated module under
viewer_loop/with a context struct that holds only the state you need. Wire the helper intoViewerLoop::window_eventbeside the existing keyboard/menu helpers to minimise borrow complexity and keep diffs localised. - Rendering updates go through
rebuild_render_data(orchestration.rs) which regeneratesRenderSceneDataand refreshes measurement overlays.request_redrawandrequest_redraw_with_titlehandle winit redraw scheduling while keeping the window title up to date with scene metadata and FPS counters.
8.2 UI State & Panels
UiState(ui/shell/src/ui_state/base.rs,ui/shell/src/ui_state/mod.rs) is the central model for persistent viewer state and is composed from focused sub-modules:SelectionUiState(ui/shell/src/ui_state/selection_state.rs) for highlights, history, and selection tools.PlacementUiState(ui/shell/src/ui_state/placement_state.rs) for edit-mode placement and change-element scope.ViewportUiState(ui/shell/src/ui_state/viewport_ui_state.rs) withViewUiStateandCameraUiState(ui/shell/src/ui_state/view_state.rs,ui/shell/src/ui_state/camera_state.rs).StyleUiState(ui/shell/src/ui_state/style_ui_state.rs) housing appearance presets and theme settings.ChromeUiState(ui/shell/src/ui_state/chrome_ui_state.rs) for panel visibility and dialogs.MarkupUiState(ui/shell/src/ui_state/markup_ui_state.rs) combining labels and annotations.OverlayUiState(ui/shell/src/ui_state/overlay_ui_state.rs) plus background overlay jobs. unified panel visibility, selection history, measurement buffers, trajectory playback, theme, orbital settings, and annotations.- Theme settings expose bond-specific controls (
bond_thickness,bond_inactive_alpha,bond_highlight_scale,bond_lane_spacing,bond_dash_scale,bond_glossiness) that feed intoapply_theme_stylesfor immediate renderer updates. - the per-frame redraw (driven from
ViewerLoop::about_to_wait) samplesegui::Context::available_rect()each frame and passes the resulting viewport to both the renderer and input handlers so docked panels reserve space instead of overlaying the 3D scene; picking and gizmo drags clamp to that rect. - Export state (
image_export_settings,image_export_profile,export_custom_presets,batch_export_profiles,batch_export_customs) includes colour-profile metadata, explicit format flags (PNG/TIFF/JPEG/SVG/PDF/EPS), theme presets for export-only styling, and post-processing controls (exposure/contrast/saturation/vignette/grain/bloom/sharpen). The export dialog reads/writes those fields, enforces per-format transparency rules, and shares the queue/batch export helpers with session persistence so saved sessions remember which presets were staged. - Annotation overlays use
orbitron_viewer_core::ui_state::AnnotationStateinsideMarkupUiState(ui/shell/src/ui_state/markup_ui_state.rs) and are rendered viaviewer_loop/annotation_overlay.rs. The Annotations panel (panels/annotations.rs) edits text/arrow items; export rendering composites them inexport/rendering/raster/overlays.rs. - Selection tooling lives on
UiState(active selection tool, history, highlights). There is no longer a Selection panel: it was dissolved into the toolbar, which owns the tool picker, the selected count, and Clear, while per-atom labels and the PDB structure controls moved into Appearance (see the comment inpanels/unified.rs). Viewport drag handling lives inviewer_loop/runtime/window_events/selection_drag.rs(right-drag box/lasso). The selection tool can also be cycled viaShift+S(edit mode) and is surfaced in the Selection menu. Edit commands invoked from panels use the edit helpers to emit status toasts for user feedback. - Edit placement state lives on
UiState(seePlacementUiState). The Edit panel toggles placement, the change-element dialog supports a periodic table picker, andChangeElementScopeseparates placement-only updates from selection-only edits so “Choose…” doesn’t mutate highlighted atoms. - The Edit panel owns the molecular charge/multiplicity controls. Menu actions open the SMILES, conformer, residue-mutation, side-chain-repair, alternate-location, water/ion-cleanup, terminal-cap, residue-protonation, and disulfide dialogs. Conformer selection previews coordinates without mutating history; accepting applies one
ApplyConformerCommand, while cancel restores the base scene. Residue mutation uses the same detached-preview pattern and shows the measured backbone angles, selected Top8000 bin, rotamer probability, chi angles, and geometric clash score before applying one exactMutateResidueCommand. Missing-side-chain repair adds source-fragment RMS and maximum displacement, missing-atom and explicit-hydrogen changes, and clash counts before and after. Choosing a row shows its complete detached scene; Apply records oneRepairSideChainCommand. Alternate-location editing previews one residue-local keep-only or delete operation. It reports shared atoms, affected atom names, location labels, and incident bonds before applying oneEditAlternateLocationCommand. Water/ion cleanup previews an exact whole-residue All/Within/Beyond match, can return the matches to ordinary atom selection, or applies oneRemoveWaterIonResiduesCommand. The live viewport always renders the same detached scene summarized by the dialog. Terminal capping previews the assigned ACE/NME residue, removed atoms, charge change, and heavy-atom geometry before one exactAddTerminalCapCommand. Residue protonation follows the same detached-scene pattern. It shows the explicit state, acidic-oxygen tautomer, residue and molecular formal charges, and removed or added hydrogens before one exactSetResidueProtonationCommand. Disulfide editing previews the two author residue sites, minimum-image SG distance, hydrogen changes, SSBOND state, and molecular formal charge before applying one exactEditDisulfideCommand. While that modal preview is open, the redraw path dismisses other edit dialogs and palettes before rendering. This prevents a concealed operation from receiving a click through the disulfide window. - Presentation mode is a simple
boolonUiState. When toggled it hides the toolbar/status bar/unified panel, shrinks panel chrome, and bumpstheme.label_font_size/label_outline_widthbefore drawing so demos keep atoms legible even on secondary displays. Saved sessions restore the mode throughSessionState::presentation_mode; a new session resets it to the default.
- For the user-facing walkthrough of these panels see the User Guide in this directory (§3 Graphical Viewer).
- Panels are grouped by feature domain:
panels/contains egui layouts for analysis (including the unified Surfaces tab that replaced the old orbital-visualisation panel), tasks, and status overlays. The Tasks view owns method/basis/solvation provenance and SCF convergence plots; Populations owns computed bond-order tables; Bands & DOS owns the linked first-Brillouin-zone window.panels/mod.rsties them together. The toolbar lives intoolbar/and defines theToolbarActionenum (toolbar/mod.rs). Beyond the render-mode switcher it now owns the selection tool picker, the selected-atom count, and Clear (ToolbarAction::ClearSelection, intoolbar/session.rs) — the controls that used to be a Selection panel — plus camera stepping viaToolbarAction::{ZoomIn, ZoomOut}.appearance.rs,tasks.rs,fragments.rs, and themeasurement/module house feature-specific controllers invoked from the panels.preferences.rsrenders the “Preferences” window (preferences_window), with sections for “File loading limits” and “Symmetry”, persisting configuration, and pushing toast notifications back intoUiState.- Selection logic is shared via the
viewer_state/module —push_historyanddescribe_gaussian_calcinviewer_state/helpers.rs,focus_selectioninviewer_state/camera.rs— so the keyboard handler, panels, and background tasks remain in sync. - Edit actions live under
viewer_state/edit/. Molecular and atom-property actions remain inactions.rs; periodic cell transforms and slab commands are grouped inactions/cell.rsand retain the same public menu-action API.
- Camera interactions originate from
camera.rs(CameraController::update_camera) and feed into event handling utilities inevent_handlers.rs.
8.2.1 Analysis panel: atom-coloring halo system
The Analysis panel went through a significant redesign in early 2026. The user-facing description is in the User Guide §3.6; here are the moving parts:
Halo overlay rendering. Every atom carries a per-instance halo: vec4 slot on AtomInstance (core/render/src/data_builder/types.rs). When halo.a > 0, the atom shader (core/render/src/renderer/shaders/atom.wgsl) paints a sign-aware rim glow over the element color: red rim for positive values, blue for negative, intensity proportional to halo.a. The shader uses a mix(final_color, halo.rgb, blend) with a falloff that combines disc (dist_sq^3) and silhouette (1 - |dot(view_dir, world_normal)|) terms — concentrated at the rim but with a 0.25 base tint floor so high-magnitude atoms read clearly even at the disc center. Element color stays at the disc center so identity (which element) and value (charge / contribution) read independently.
Scheme state lives on AnalysisPanelState.atom_halo: AtomHaloState (viewer/core/src/ui_state/atom_color_scheme.rs). The AtomColorScheme enum picks among:
NoneMullikenCharges/LowdinCharges/NaturalCharges/AptCharges— backed byElectronicStructure.{mulliken,lowdin,natural,apt}_population.SceneMoContribution { mo_index, spin }— Σ|c|² per atom signed by which lobe (positive vs negative coefficient sum) dominates.NboContribution { family, index }— same convention but pulled fromNboWorkspace.signed_atom_contributions_for(family, index)since NBO data lives outside the backbone scene graph.
AnalysisPanelState::recompute_atom_halo(scene) fills in atom_halo.values for the charges and SceneMo schemes. NBO contribution values are filled in by ui-shell (atom_coloring-equivalent code path inside panels/analysis/nbo.rs) since NboWorkspace lives there.
Rendering pipeline. apply_halo_overlay(data, halo_state) in ui/shell/src/render_helpers.rs walks halo_state.values and writes each atom’s halo slot via apply_atom_halos() (core/render/src/data_builder/halos.rs). Called from orchestration/render.rs::rebuild_render_data_with_hidden_atoms. (The old apply_charge_colors step was removed — atom coloring now flows entirely through the halo overlay.)
Render cache invalidation. RenderCacheKey (ui/shell/src/render_cache/key.rs) hashes atom_halo so changing the scheme invalidates the cache. The hashing function (hashing.rs::hash_atom_halo) covers scheme + per-atom values + max_abs + colors + max_opacity. Without the cache key trip, apply_halo_overlay would be skipped on the next redraw. The same rule applies to every render-affecting view flag: any new field that changes RenderSceneData must be added to RenderCacheKey (e.g. focus_dim_unselected, atoms_only_for_selection) or toggling it hits a stale cache entry and appears dead.
Render rebuild trigger. Activating a halo from any tab must set atom_coloring_changed = true in AnalysisPanelActions (ui/shell/src/panels/analysis_panel.rs). The viewer-loop dispatcher (viewer_loop/runtime/redraw/panels/mod.rs) maps that to should_rebuild_scene = true. Without this trip the render data isn’t rebuilt and the halo never paints.
UI surfaces. No standalone “Atom Coloring” tab — entry points live in their natural homes:
- Charges tab (
panels/analysis/population/) — per-scheme “Show as halo” / “Disable halo” buttons.apply_population_actioninactions.rssets the halo scheme and callsrecompute_atom_halo. - Orbitals tab (
panels/analysis/molecular.rs) — per-MO collapsible has a “Show as halo” button that resolves the spin block (Up/Down/Total) fromn_alphaand the global MO index. - NBO tab (
panels/analysis/nbo.rs) — “Show as halo” on the selected NBO orbital. Action isNboAction::ShowAsHalo { family, index }; the dispatcher maps the ioOrbitalFamilyto the ui-stateNboFamilyTagand stages the per-atom values. - Slice tab (
panels/analysis/slice.rs,AnalysisSubtab::Sliceinviewer/core/src/ui_state/analysis.rs, rendered frompanels/analysis_panel.rs) — a 2D MO/density contour on a plane through 3 picked atoms; gated on a parsed basis set. Distinct from the halo schemes (it draws its own heatmap, not an atom halo). - Halo appearance expander (
panels/analysis/halo_appearance.rs) — color pickers + max-opacity slider. Each tab callssuper::render_halo_appearance(ui, ui_state)at the bottom; the helper short-circuits when no halo is active.
Surfaces panel (panels/analysis/orbital_surfaces/) is independent of the halo system — it manages 3D isosurface meshes via SurfaceEntry { source: OrbitalSource, render: bool, mesh: ... }. reconcile_surface_entries walks the scene’s electronic structure + cube datasets + NBO workspace each frame and populates one row per source. “Compute selected” runs the appropriate generator (cube → marching cubes, SceneMo → convert_fchk_basis + generate_nbo_orbital_meshes_with_basis, NBO → workspace coefficients + same generator).
8.3 Detached export window and file exporters
File > Export has ten commands. Only Export Image… opens the detached window (ui/shell/src/menu/file_menu.rs):
Export Image… →
MenuAction::Export, the detached export window described below.Export XYZ… / Export PDB… →
MenuAction::{ExportXyz, ExportPdb}, handled inline inviewer_loop/menu_actions/files.rsover the writers inexport/utils.rs; both write all atoms plus visible overlays.Export GROMACS GRO… →
MenuAction::ExportGro, which writes the base scene, residue identity, velocities, and periodic box throughExporterService::export_gro. It reports every field that GRO cannot carry.Export POSCAR… →
MenuAction::ExportPoscar, which requires a unit cell and preserves frozen atoms as Selective dynamics flags.Export Molfile… →
MenuAction::ExportMolfile, which writes the base scene and connectivity. It deliberately excludes overlays because a Molfile is a connection table rather than a composed view.Export Web View… →
MenuAction::ExportWebView(export/web_view.rs+export/web_view_template.html): a self-contained HTML file with the wasm viewer embedded, opening to the current view. Everything the bare scene JSON cannot carry — cartoon ribbons, isosurfaces, halos, cell overlays, the Slice plane, the vibrational modes — is gathered into aDisplayCaptureand serialized as a delta envelope byexport/render_deltas.rs, which the page applies viaapplyDeltaafterscene-loaded. Adding another one means a field onDisplayCaptureand an op; if the op is new,core/delta-types’ enum is shared with the wasm, whose exhaustive match will force you to handle it there.Export Bundle… →
MenuAction::ExportBundle: the source’s canonical document and attachments plus the current camera, surfaces, halos, and appearance. Save Session… uses the same container and also records desktop-window state.Export Appearance Bundle… →
MenuAction::ExportAppearanceBundle: theme, camera, diagram, and cartoon settings as portable JSON, replayable in the WASM viewer or viaorbitron render.Import Appearance Bundle… →
MenuAction::ImportAppearanceBundle: applies that portable JSON to the current scene without replacing its scientific data.Detached popups are managed by
ExportWindowManager(ui/shell/src/export/manager.rs), which tracksWindowId→WindowKind(currently only Export). Routing is keyed onis_managed_window(window_id)insideViewerLoop::window_event.Export window opens via
MenuAction::Export→runtime.export_window_open_request→ theabout_to_waithandler →open_export, and is torn down byclose_export_windowwhen it closes or export completes.runtime.export_window_idrecords the active window.Export host (
export/host.rs) owns its own winit window + wgpu surface/device for egui painting. It reuses the main renderer for preview/export rendering but snapshotsUiState.exporton open and restores it on cancel/close (kept when an export succeeds).To add another popup: add a
WindowKindvariant and corresponding open/close/redraw/handle methods in the manager, add a runtime open-request flag, wireabout_to_waitto spawn it, and route events/redraws byWindowId. Keep window-specific snapshot/restore logic near each host.
8.4 Background workflows
- Long-running operations (load new file, parse task summaries, export orbitals) run via the
backgroundmodule which spawns threads sendingProgressMsgupdates over channels.CancelHandleallows the user to abort transfers; the viewer loop pollsbg_rx/tasks_rxeach frame.