Skip to main content

khora_infra/ui/egui/app/
context.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//! Concrete [`AppContext`] implementation backed by `egui::Context`.
10
11use std::collections::HashMap;
12
13use khora_core::ui::editor::ViewportTextureHandle;
14use khora_core::ui::{AppContext, FontPack, UiBuilder, UiTheme};
15
16use crate::ui::egui::theme::apply_theme;
17use crate::ui::egui::ui_builder::EguiUiBuilder;
18
19/// Wraps the frame's root `egui::Ui` to implement [`AppContext`].
20///
21/// Constructed once per frame by [`super::run_native`] and handed to
22/// the user's [`khora_core::ui::App::update`] implementation.
23///
24/// Holds a `Ui` rather than a `Context` because eframe hands the app a root
25/// `Ui` to paint *into*; panels nest inside it with `show_inside`. The
26/// `Context` is still reachable through it for the theme, fonts and viewport
27/// commands, which are context-wide rather than panel-local.
28pub struct EguiAppContext<'a> {
29    ui: &'a mut egui::Ui,
30    frame: &'a mut eframe::Frame,
31    /// No viewport textures for tool apps — empty map, kept so we can
32    /// reuse `EguiUiBuilder::new`.
33    viewport_textures: HashMap<ViewportTextureHandle, egui::TextureId>,
34}
35
36impl<'a> EguiAppContext<'a> {
37    /// Build a context from the per-frame egui handles.
38    pub fn new(ui: &'a mut egui::Ui, frame: &'a mut eframe::Frame) -> Self {
39        Self {
40            ui,
41            frame,
42            viewport_textures: HashMap::new(),
43        }
44    }
45}
46
47impl AppContext for EguiAppContext<'_> {
48    fn central(&mut self, f: &mut dyn FnMut(&mut dyn UiBuilder)) {
49        let vt = &self.viewport_textures;
50        egui::CentralPanel::default()
51            .frame(egui::Frame::new())
52            .show_inside(self.ui, |ui| {
53                let mut builder = EguiUiBuilder::new(ui, vt);
54                f(&mut builder);
55            });
56    }
57
58    fn set_theme(&mut self, theme: &UiTheme) {
59        let ctx = self.ui.ctx().clone();
60        apply_theme(&ctx, theme);
61    }
62
63    fn set_fonts(&mut self, pack: &FontPack) {
64        if pack.is_empty() {
65            return;
66        }
67        use std::sync::Arc;
68        let ctx = self.ui.ctx().clone();
69        let mut defs = egui::FontDefinitions::default();
70        install_family(
71            &mut defs,
72            egui::FontFamily::Proportional,
73            &pack.proportional,
74        );
75        install_family(&mut defs, egui::FontFamily::Monospace, &pack.monospace);
76        if !pack.display.is_empty() {
77            install_family(
78                &mut defs,
79                egui::FontFamily::Name("display".into()),
80                &pack.display,
81            );
82        }
83        if !pack.icons.is_empty() {
84            install_family(
85                &mut defs,
86                egui::FontFamily::Name("icons".into()),
87                &pack.icons,
88            );
89        }
90        // Both named families must always resolve: epaint *panics* when text
91        // asks for a `FontFamily::Name` that is bound to no fonts. Appending
92        // the proportional faces as a fallback means a missing Fraunces
93        // degrades to Geist and a missing icon font degrades to tofu — never
94        // to a crash.
95        for named in ["display", "icons"] {
96            let fallback = defs
97                .families
98                .get(&egui::FontFamily::Proportional)
99                .cloned()
100                .unwrap_or_default();
101            let entry = defs
102                .families
103                .entry(egui::FontFamily::Name(named.into()))
104                .or_default();
105            for key in fallback {
106                if !entry.contains(&key) {
107                    entry.push(key);
108                }
109            }
110        }
111        ctx.set_fonts(defs);
112
113        fn install_family(
114            defs: &mut egui::FontDefinitions,
115            family: egui::FontFamily,
116            fonts: &[khora_core::ui::NamedFont],
117        ) {
118            for (idx, named) in fonts.iter().enumerate() {
119                let key = named.name.clone();
120                let bytes = match &named.data {
121                    khora_core::ui::FontHandle::Static(s) => s.to_vec(),
122                    khora_core::ui::FontHandle::Owned(v) => v.clone(),
123                };
124                defs.font_data
125                    .insert(key.clone(), Arc::new(egui::FontData::from_owned(bytes)));
126                let entry = defs.families.entry(family.clone()).or_default();
127                if idx == 0 {
128                    entry.insert(0, key);
129                } else {
130                    entry.push(key);
131                }
132            }
133        }
134    }
135
136    fn screen_size(&self) -> [f32; 2] {
137        let r = self
138            .ui
139            .ctx()
140            .input(|i| i.viewport().inner_rect.unwrap_or(egui::Rect::ZERO));
141        [r.width(), r.height()]
142    }
143
144    fn pixels_per_point(&self) -> f32 {
145        self.ui.ctx().pixels_per_point()
146    }
147
148    fn request_repaint(&mut self) {
149        self.ui.ctx().request_repaint();
150    }
151
152    fn request_close(&mut self) {
153        self.ui
154            .ctx()
155            .send_viewport_cmd(egui::ViewportCommand::Close);
156        let _ = &self.frame; // mark used
157    }
158}