UI Shell Architecture

8.1 Viewer Loop

  • ViewerLoop (ui/shell/src/viewer_loop/mod.rs) owns the winit event loop, renderer, egui context, camera controller, and complete viewer state. It is responsible for translating winit::event::Event 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/, remote_browser.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::run 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, annotations, and remote browsing preferences.
    • 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.
    • ViewerLoop::run 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.rs.
    • Selection tooling lives on UiState (active selection tool, history, highlights) and is driven by the Selection panel (panels/selection.rs) plus viewport drag handling in viewer_loop/runtime/window_events.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.
    • 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. The mode is transient (sessions do not force other users into presentation-only layouts).
  • 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; panels/mod.rs ties them together. The toolbar lives in toolbar/ and defines the ToolbarAction enum (toolbar/mod.rs).
    • appearance.rs, tasks.rs, fragments.rs, and the measurement/ module house feature-specific controllers invoked from the panels.
    • preferences.rs renders the “Remote Data Settings” dialog, wiring RemoteSettingsForm to OrbitronConfig. It handles health checks, persisting configuration, and pushing toast notifications back into UiState.
    • Selection logic is shared via viewer_state.rs (push_history, focus_selection, describe_gaussian_calc, etc.) so the keyboard handler, panels, and background tasks remain in sync.
  • Camera interactions originate from camera.rs (CameraController::update_camera) and feed into event handling utilities in event_handlers.rs.

8.3.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.
  • 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.4 Detached Windows (Export popup, future tools)

  • 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::run.
  • Export window opens via MenuAction::Exportruntime.export_window_open_requestAboutToWaitopen_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 AboutToWait to spawn it, and route events/redraws by WindowId. Keep window-specific snapshot/restore logic near each host.

8.3 Background & Remote 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.
  • RemoteContext (remote/) tracks the configured target, cached directory listings, and pending remote downloads. RemoteContext::from_sources merges persisted config with the ORBITRON_REMOTE environment override and holds a shared Arc<SftpDataSource>; background listing/download threads call it (remote/context/jobs.rs). The browser overlay issues actions (RemoteBrowserAction) handled by ViewerApp::handle_remote_action.
  • Menu wiring disables Open Remote… until a target is configured. Configure Remote… surfaces the preferences dialog; successful saves update UiState and persist via OrbitronConfig::save_default.
  • Remote access goes through the SftpDataSource (orbitron-services) DataSource implementation, so the UI shares code paths with headless automation when browsing remote datasets — there is no separate server or HTTP client.