UI Shell Architecture

8.1 Viewer Loop

  • ViewerApplication (ui/shell/src/runner.rs) owns the winit event loop: under winit 0.30 it implements winit::application::ApplicationHandler (resumed / window_event / device_event / about_to_wait) and is driven by event_loop.run_app(&mut app).
  • ViewerLoop (ui/shell/src/viewer_loop/runtime/state.rs, re-exported via mod.rs) owns the renderer, egui context, camera controller, and complete viewer state. ViewerApplication forwards each ApplicationHandler callback to ViewerLoop’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 via KeyActionContext.
    • menu_actions/ mirrors the GUI menu tree (the menu/ module) and triggers async loads, toggles, and exports.
    • progress_overlay.rs, fragment_palette.rs, change_element/, smiles_builder_dialog.rs, conformer_dialog.rs, and residue_mutation_dialog.rs, residue_protonation_dialog.rs, terminal_cap_dialog.rs, and disulfide_dialog.rs isolate complex overlays/dialogs for readability.
    • dialog_context.rs provides 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 into ViewerLoop::window_event beside the existing keyboard/menu helpers to minimise borrow complexity and keep diffs localised.
  • Rendering updates go through rebuild_render_data (orchestration.rs) which regenerates RenderSceneData and refreshes measurement overlays. request_redraw and request_redraw_with_title handle 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) with ViewUiState and CameraUiState (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 into apply_theme_styles for immediate renderer updates.
    • the per-frame redraw (driven from ViewerLoop::about_to_wait) samples egui::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::AnnotationState inside MarkupUiState (ui/shell/src/ui_state/markup_ui_state.rs) and are rendered via viewer_loop/annotation_overlay.rs. The Annotations panel (panels/annotations.rs) edits text/arrow items; export rendering composites them in export/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 in panels/unified.rs). Viewport drag handling lives in viewer_loop/runtime/window_events/selection_drag.rs (right-drag box/lasso). The selection tool can also be cycled via Shift+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 (see PlacementUiState). The Edit panel toggles placement, the change-element dialog supports a periodic table picker, and ChangeElementScope separates 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 exact MutateResidueCommand. 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 one RepairSideChainCommand. 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 one EditAlternateLocationCommand. Water/ion cleanup previews an exact whole-residue All/Within/Beyond match, can return the matches to ordinary atom selection, or applies one RemoveWaterIonResiduesCommand. 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 exact AddTerminalCapCommand. 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 exact SetResidueProtonationCommand. Disulfide editing previews the two author residue sites, minimum-image SG distance, hydrogen changes, SSBOND state, and molecular formal charge before applying one exact EditDisulfideCommand. 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 bool on UiState. When toggled it hides the toolbar/status bar/unified panel, shrinks panel chrome, and bumps theme.label_font_size/label_outline_width before drawing so demos keep atoms legible even on secondary displays. Saved sessions restore the mode through SessionState::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.rs ties them together. The toolbar lives in toolbar/ and defines the ToolbarAction enum (toolbar/mod.rs). Beyond the render-mode switcher it now owns the selection tool picker, the selected-atom count, and Clear (ToolbarAction::ClearSelection, in toolbar/session.rs) — the controls that used to be a Selection panel — plus camera stepping via ToolbarAction::{ZoomIn, ZoomOut}.
    • appearance.rs, tasks.rs, fragments.rs, and the measurement/ module house feature-specific controllers invoked from the panels.
    • preferences.rs renders the “Preferences” window (preferences_window), with sections for “File loading limits” and “Symmetry”, persisting configuration, and pushing toast notifications back into UiState.
    • Selection logic is shared via the viewer_state/ module — push_history and describe_gaussian_calc in viewer_state/helpers.rs, focus_selection in viewer_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 in actions.rs; periodic cell transforms and slab commands are grouped in actions/cell.rs and retain the same public menu-action API.
  • Camera interactions originate from camera.rs (CameraController::update_camera) and feed into event handling utilities in event_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:

  • None
  • MullikenCharges / LowdinCharges / NaturalCharges / AptCharges — backed by ElectronicStructure.{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 from NboWorkspace.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_action in actions.rs sets the halo scheme and calls recompute_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) from n_alpha and the global MO index.
  • NBO tab (panels/analysis/nbo.rs) — “Show as halo” on the selected NBO orbital. Action is NboAction::ShowAsHalo { family, index }; the dispatcher maps the io OrbitalFamily to the ui-state NboFamilyTag and stages the per-atom values.
  • Slice tab (panels/analysis/slice.rs, AnalysisSubtab::Slice in viewer/core/src/ui_state/analysis.rs, rendered from panels/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 calls super::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 in viewer_loop/menu_actions/files.rs over the writers in export/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 through ExporterService::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 a DisplayCapture and serialized as a delta envelope by export/render_deltas.rs, which the page applies via applyDelta after scene-loaded. Adding another one means a field on DisplayCapture and 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 via orbitron 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 tracks WindowIdWindowKind (currently only Export). Routing is keyed on is_managed_window(window_id) inside ViewerLoop::window_event.

  • Export window opens via MenuAction::Exportruntime.export_window_open_request → the about_to_wait handler → open_export, and is torn down by close_export_window when it closes or export completes. runtime.export_window_id records 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 snapshots UiState.export on open and restores it on cancel/close (kept when an export succeeds).

  • To add another popup: add a WindowKind variant and corresponding open/close/redraw/handle methods in the manager, add a runtime open-request flag, wire about_to_wait to spawn it, and route events/redraws by WindowId. 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 background module which spawns threads sending ProgressMsg updates over channels. CancelHandle allows the user to abort transfers; the viewer loop polls bg_rx / tasks_rx each frame.