Skip to main content

khora_editor/
bootstrap.rs

1// Copyright 2025 eraflo
2//
3// Licensed under the Apache License, Version 2.0 (the "License");
4// you may not use this file except in compliance with the License.
5// You may obtain a copy of the License at
6//
7//     http://www.apache.org/licenses/LICENSE-2.0
8
9//! Editor bootstrap: CLI parsing, window icon, winit + overlay/shell setup.
10//!
11//! Owns the binary entry point and the closure handed to `run_winit` that
12//! constructs the renderer, the egui overlay, and the editor shell. Keeps
13//! `app.rs` focused on `EditorApp` and `EngineApp` semantics.
14
15use std::sync::{Arc, Mutex};
16
17use khora_sdk::khora_core::ui::EditorOverlay;
18use khora_sdk::prelude::*;
19use khora_sdk::run_winit;
20use khora_sdk::winit;
21use khora_sdk::winit_adapters::WinitWindowProvider;
22use khora_sdk::{
23    AudioDevice, AudioMixBus, AudioStream, CpalAudioDevice, DefaultMixBus, EditorShell,
24    LayoutSystem, PhysicsProvider, PipelineSystem, RapierPhysicsWorld, RenderSystem,
25    StandardTextRenderer, StreamInfo, TaffyLayoutSystem, TextRenderer, WgpuPipelineSystem,
26    WgpuRenderSystem, TEXT_WGSL,
27};
28
29use crate::app::EditorApp;
30
31/// CLI project path passed via `--project <path>`.
32pub static PROJECT_PATH: std::sync::OnceLock<Option<String>> = std::sync::OnceLock::new();
33
34/// Decode the embedded PNG logo into a `WindowIcon`.
35pub fn load_logo_icon() -> WindowIcon {
36    let png_bytes = include_bytes!("../assets/khora_small_logo.png");
37    match image::load_from_memory(png_bytes) {
38        Ok(img) => {
39            let rgba_img = img.to_rgba8();
40            let (w, h) = rgba_img.dimensions();
41            WindowIcon {
42                rgba: rgba_img.into_raw(),
43                width: w,
44                height: h,
45            }
46        }
47        Err(e) => {
48            log::warn!("Failed to decode logo PNG: {}", e);
49            WindowIcon {
50                rgba: vec![0, 0, 0, 0],
51                width: 1,
52                height: 1,
53            }
54        }
55    }
56}
57
58/// Editor binary entry point. Parses `--project`, then hands control to
59/// `run_winit` with a setup closure that wires renderer + overlay + shell.
60pub fn run() -> anyhow::Result<()> {
61    let args: Vec<String> = std::env::args().collect();
62    let project = args
63        .windows(2)
64        .find(|w| w[0] == "--project")
65        .map(|w| w[1].clone());
66    let _ = PROJECT_PATH.set(project);
67
68    run_winit::<WinitWindowProvider, EditorApp>(|window, runtime, event_loop_any| {
69        let mut rs = WgpuRenderSystem::new();
70        rs.init(window).expect("renderer init failed");
71        runtime.backends.insert(rs.graphics_device());
72
73        // Build the editor overlay (egui) + shell (dock + panels) so the
74        // editor UI renders on top of the 3D scene each frame.
75        let event_loop = event_loop_any
76            .downcast_ref::<winit::event_loop::ActiveEventLoop>()
77            .expect("editor: bootstrap expects a winit ActiveEventLoop");
78        let theme = khora_sdk::khora_core::ui::UiTheme::default();
79        match rs.create_editor_overlay_and_shell(
80            event_loop,
81            khora_sdk::khora_lanes::render_lane::shaders::EGUI_WGSL,
82            theme,
83            khora_sdk::PRIMARY_VIEWPORT,
84        ) {
85            Ok((overlay, shell)) => {
86                let overlay: Box<dyn EditorOverlay> = Box::new(overlay);
87                let shell: Box<dyn EditorShell> = Box::new(shell);
88                runtime.backends.insert(Arc::new(Mutex::new(overlay)));
89                runtime.backends.insert(Arc::new(Mutex::new(shell)));
90                log::info!("editor: overlay + shell created");
91            }
92            Err(e) => {
93                log::error!("editor: failed to create overlay+shell: {e:?}");
94            }
95        }
96
97        let rs: Box<dyn RenderSystem> = Box::new(rs);
98        runtime.backends.insert(Arc::new(Mutex::new(rs)));
99
100        // Shader / pipeline backend — wgpu + naga_oil. The app picks the
101        // backend; the engine core consumes it as `Arc<dyn PipelineSystem>`.
102        match WgpuPipelineSystem::new() {
103            Ok(sys) => {
104                let sys: Arc<dyn PipelineSystem> = Arc::new(sys);
105                runtime.resources.insert(sys);
106            }
107            Err(e) => log::error!("pipeline system init failed: {e}"),
108        }
109
110        // Physics — Rapier3D
111        let physics: Box<dyn PhysicsProvider> = Box::new(RapierPhysicsWorld::default());
112        runtime.backends.insert(Arc::new(Mutex::new(physics)));
113
114        // UI layout — Taffy
115        let layout: Box<dyn LayoutSystem> = Box::new(TaffyLayoutSystem::new());
116        runtime.backends.insert(Arc::new(Mutex::new(layout)));
117
118        // Text renderer — StandardTextRenderer
119        let text: Arc<dyn TextRenderer> = Arc::new(StandardTextRenderer::new(TEXT_WGSL.to_owned()));
120        runtime.backends.insert(text);
121
122        // Audio — shared mix bus + CPAL device. The bus is the sole
123        // synchronisation boundary between audio lanes (main thread) and
124        // the backend's hardware callback (RT thread). The opened
125        // AudioStream is stored as a backend handle; dropping it stops
126        // the stream.
127        let stream_info = StreamInfo {
128            channels: 2,
129            sample_rate: 48_000,
130        };
131        let mix_bus: Arc<dyn AudioMixBus> = Arc::new(DefaultMixBus::new(stream_info, 8192));
132        runtime.resources.insert(Arc::clone(&mix_bus));
133        let device: Box<dyn AudioDevice> = Box::new(CpalAudioDevice::new());
134        match device.open(mix_bus) {
135            Ok(stream) => {
136                let stream: Arc<dyn AudioStream> = Arc::from(stream);
137                runtime.backends.insert(stream);
138            }
139            Err(e) => log::error!("audio open failed: {}", e),
140        }
141    })?;
142    Ok(())
143}