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}