Skip to main content

khora_infra/ui/egui/app/
runtime.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//! `run_native` — boots an [`khora_core::ui::App`] under eframe.
10//!
11//! This is the concrete equivalent of `eframe::run_native`: it sets
12//! the window title / icon / size from a [`WindowConfig`], drives the
13//! per-frame `update` against the user's `App` impl through an
14//! [`EguiAppContext`] adapter, and surfaces a friendly error type on
15//! boot failure.
16
17use anyhow::{anyhow, Result};
18
19use khora_core::ui::App;
20
21use super::EguiAppContext;
22
23/// Bridge struct that adapts a `Box<dyn App>` to the `eframe::App`
24/// trait so we can hand it to `eframe::run_native`.
25struct AppAdapter {
26    inner: Box<dyn App>,
27    started: bool,
28}
29
30impl eframe::App for AppAdapter {
31    // eframe hands the app a root `Ui` to paint into rather than a `Context`
32    // to open panels against.
33    fn ui(&mut self, ui: &mut egui::Ui, frame: &mut eframe::Frame) {
34        if !self.started {
35            {
36                let mut adapter = EguiAppContext::new(ui, frame);
37                self.inner.on_start(&mut adapter);
38            }
39            self.started = true;
40
41            // `on_start` is where the app installs its fonts, but
42            // `Context::set_fonts` only takes effect at the *next* pass — we
43            // are already inside this one. Painting now would lay text out
44            // against egui's built-in fonts, and any widget asking for a named
45            // family (`icons`, `display`) would panic because that family does
46            // not exist yet. So skip drawing this frame and come straight back
47            // with the real fonts loaded. One blank frame at startup, and no
48            // app has to know about the ordering.
49            ui.ctx().request_repaint();
50            return;
51        }
52
53        let mut adapter = EguiAppContext::new(ui, frame);
54        self.inner.update(&mut adapter);
55    }
56
57    fn on_exit(&mut self, _gl: Option<&eframe::glow::Context>) {
58        self.inner.on_exit();
59    }
60}
61
62/// Boot a tool app on the egui+eframe backend.
63///
64/// `factory` is called once after the renderer is up and must return
65/// the `App` instance. Mirrors `eframe::run_native`'s creation
66/// callback shape but receives the neutral context type.
67pub fn run_native<F>(window: WindowConfigInput, factory: F) -> Result<()>
68where
69    F: FnOnce() -> Box<dyn App> + 'static,
70{
71    let mut viewport = egui::ViewportBuilder::default()
72        .with_title(window.title.clone())
73        .with_inner_size([window.width as f32, window.height as f32]);
74    if let Some(icon) = window.icon {
75        viewport = viewport.with_icon(egui::IconData {
76            rgba: icon.rgba,
77            width: icon.width,
78            height: icon.height,
79        });
80    }
81    let native_options = eframe::NativeOptions {
82        viewport,
83        ..Default::default()
84    };
85    eframe::run_native(
86        &window.title,
87        native_options,
88        Box::new(move |_cc| {
89            let app = factory();
90            Ok(Box::new(AppAdapter {
91                inner: app,
92                started: false,
93            }) as Box<dyn eframe::App>)
94        }),
95    )
96    .map_err(|e| anyhow!("eframe::run_native failed: {}", e))?;
97    Ok(())
98}
99
100/// Owned-data window config — mirrors fields of `WindowConfig` but
101/// stays in `khora-infra` so we don't drag the platform module
102/// definition all the way down to `khora-core` for tool-only use.
103#[derive(Debug, Clone)]
104pub struct WindowConfigInput {
105    /// Window title.
106    pub title: String,
107    /// Initial width in pixels.
108    pub width: u32,
109    /// Initial height in pixels.
110    pub height: u32,
111    /// Optional window icon.
112    pub icon: Option<WindowIconInput>,
113}
114
115impl Default for WindowConfigInput {
116    fn default() -> Self {
117        Self {
118            title: "Khora Tool".to_owned(),
119            width: 1024,
120            height: 720,
121            icon: None,
122        }
123    }
124}
125
126/// RGBA8 pixel buffer + dimensions for the window icon.
127#[derive(Debug, Clone)]
128pub struct WindowIconInput {
129    /// Row-major RGBA8 pixels.
130    pub rgba: Vec<u8>,
131    /// Width in pixels.
132    pub width: u32,
133    /// Height in pixels.
134    pub height: u32,
135}