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 translatingwinit::event::Eventinto 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/,remote_browser.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::runbeside 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, 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 intoapply_theme_stylesfor immediate renderer updates. ViewerLoop::runsamplesegui::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.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 inviewer_loop/runtime/window_events.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. - 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. 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.rsties them together. The toolbar lives intoolbar/and defines theToolbarActionenum (toolbar/mod.rs).appearance.rs,tasks.rs,fragments.rs, and themeasurement/module house feature-specific controllers invoked from the panels.preferences.rsrenders the “Remote Data Settings” dialog, wiringRemoteSettingsFormtoOrbitronConfig. It handles health checks, persisting configuration, and pushing toast notifications back intoUiState.- 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 inevent_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:
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. - 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.4 Detached Windows (Export popup, future tools)
- 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::run. - Export window opens via
MenuAction::Export→runtime.export_window_open_request→AboutToWait→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, wireAboutToWaitto spawn it, and route events/redraws byWindowId. 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
backgroundmodule which spawns threads sendingProgressMsgupdates over channels.CancelHandleallows the user to abort transfers; the viewer loop pollsbg_rx/tasks_rxeach frame. RemoteContext(remote/) tracks the configured target, cached directory listings, and pending remote downloads.RemoteContext::from_sourcesmerges persisted config with theORBITRON_REMOTEenvironment override and holds a sharedArc<SftpDataSource>; background listing/download threads call it (remote/context/jobs.rs). The browser overlay issues actions (RemoteBrowserAction) handled byViewerApp::handle_remote_action.- Menu wiring disables Open Remote… until a target is configured. Configure Remote… surfaces the preferences dialog; successful saves update
UiStateand persist viaOrbitronConfig::save_default. - Remote access goes through the
SftpDataSource(orbitron-services)DataSourceimplementation, so the UI shares code paths with headless automation when browsing remote datasets — there is no separate server or HTTP client.