bones_framework/render/
ui.rs

1//! UI resources & components.
2
3use std::sync::Arc;
4
5use crate::prelude::*;
6
7pub use ::egui;
8use serde::Deserialize;
9
10pub mod widgets;
11
12/// The Bones Framework UI plugin.
13pub fn ui_plugin(_session: &mut SessionBuilder) {
14    // TODO: remove this plugin if it remains unused.
15}
16
17/// Resource containing the [`egui::Context`] that can be used to render UI.
18#[derive(HasSchema, Clone, Debug, Default, Deref, DerefMut)]
19pub struct EguiCtx(pub egui::Context);
20
21/// [Shared resource](Game::insert_shared_resource) that, if inserted, allows you to modify the raw
22/// egui input based on the state of the last game frame.
23///
24/// This can be useful, for example, for generating arrow-key and Enter key presses from gamepad
25/// inputs.
26#[derive(HasSchema, Clone, Deref, DerefMut)]
27#[schema(no_default)]
28pub struct EguiInputHook(pub Arc<dyn Fn(&mut Game, &mut egui::RawInput) + Sync + Send + 'static>);
29
30impl EguiInputHook {
31    /// Create a new egui input hook.
32    pub fn new<F: Fn(&mut Game, &mut egui::RawInput) + Sync + Send + 'static>(hook: F) -> Self {
33        Self(Arc::new(hook))
34    }
35}
36
37/// Resource that maps image handles to their associated egui textures.
38#[derive(HasSchema, Clone, Debug, Default, Deref, DerefMut)]
39pub struct EguiTextures(pub HashMap<Handle<Image>, egui::TextureId>);
40
41impl EguiTextures {
42    /// Get the [`egui::TextureId`] for the given bones [`Handle<Image>`].
43    #[track_caller]
44    pub fn get(&self, handle: Handle<Image>) -> egui::TextureId {
45        *self.0.get(&handle).unwrap()
46    }
47}
48
49/// A font asset.
50#[derive(HasSchema, Clone)]
51#[schema(no_default)]
52#[type_data(asset_loader(["ttf", "otf"], FontLoader))]
53pub struct Font {
54    /// The name of the loaded font family.
55    pub family_name: Arc<str>,
56    /// The egui font data.
57    pub data: egui::FontData,
58    /// Whether or not this is a monospace font.
59    pub monospace: bool,
60}
61
62/// Font metadata for buttons, headings, etc, describing the font, size, and color of text to be
63/// rendered.
64#[derive(HasSchema, Debug, serde::Deserialize, Clone)]
65#[derive_type_data(SchemaDeserialize)]
66pub struct FontMeta {
67    /// The font-family to use.
68    #[serde(deserialize_with = "deserialize_arc_str")]
69    pub family: Arc<str>,
70    /// The font size.
71    pub size: f32,
72    /// The font color.
73    pub color: Color,
74}
75
76impl Default for FontMeta {
77    fn default() -> Self {
78        Self {
79            family: "".into(),
80            size: Default::default(),
81            color: Default::default(),
82        }
83    }
84}
85
86impl From<FontMeta> for egui::FontSelection {
87    fn from(val: FontMeta) -> Self {
88        egui::FontSelection::FontId(val.id())
89    }
90}
91
92impl FontMeta {
93    /// Get the Egui font ID.
94    pub fn id(&self) -> egui::FontId {
95        egui::FontId::new(self.size, egui::FontFamily::Name(self.family.clone()))
96    }
97
98    /// Create an [`egui::RichText`] that can be passed to [`ui.label()`][egui::Ui::label].
99    pub fn rich(&self, t: impl Into<String>) -> egui::RichText {
100        egui::RichText::new(t).color(self.color).font(self.id())
101    }
102
103    /// Clone the font and set a new color.
104    pub fn with_color(&self, color: Color) -> Self {
105        Self {
106            family: self.family.clone(),
107            size: self.size,
108            color,
109        }
110    }
111}
112
113fn deserialize_arc_str<'de, D: serde::Deserializer<'de>>(d: D) -> Result<Arc<str>, D::Error> {
114    String::deserialize(d).map(|x| x.into())
115}
116
117/// The [`Font`] asset loader.
118pub struct FontLoader;
119impl AssetLoader for FontLoader {
120    fn load(&self, _ctx: AssetLoadCtx, bytes: &[u8]) -> BoxedFuture<anyhow::Result<SchemaBox>> {
121        let bytes = bytes.to_vec();
122        Box::pin(async move {
123            let (family_name, monospace) = {
124                let face = ttf_parser::Face::parse(&bytes, 0)?;
125                (
126                    face.names()
127                        .into_iter()
128                        .filter(|x| x.name_id == ttf_parser::name_id::FAMILY)
129                        .find_map(|x| x.to_string())
130                        .ok_or_else(|| {
131                            anyhow::format_err!("Could not read font family from font file")
132                        })?
133                        .into(),
134                    face.is_monospaced(),
135                )
136            };
137            let data = egui::FontData::from_owned(bytes.to_vec());
138
139            Ok(SchemaBox::new(Font {
140                family_name,
141                data,
142                monospace,
143            }))
144        })
145    }
146}
147
148/// Resource for configuring egui rendering.
149#[derive(HasSchema, Clone, Debug)]
150#[repr(C)]
151pub struct EguiSettings {
152    /// Custom scale for the UI.
153    pub scale: f64,
154}
155
156impl Default for EguiSettings {
157    fn default() -> Self {
158        Self { scale: 1.0 }
159    }
160}
161
162/// Extension trait with helpers for the egui context
163pub trait EguiContextExt {
164    /// Clear the UI focus
165    fn clear_focus(self);
166
167    /// Get a global runtime state from the EGUI context, returning the default value if it is not
168    /// present.
169    ///
170    /// This is just a convenience wrapper around Egui's built in temporary data store.
171    ///
172    /// The value will be cloned to get it out of the store without holding a lock.
173    fn get_state<T: Clone + Default + Sync + Send + 'static>(self) -> T;
174
175    /// Set a global runtime state from the EGUI context.
176    ///
177    /// This is just a convenience wrapper around Egui's built in temporary data store.
178    fn set_state<T: Clone + Default + Sync + Send + 'static>(self, value: T);
179}
180
181impl EguiContextExt for &egui::Context {
182    fn clear_focus(self) {
183        self.memory_mut(|r| r.request_focus(egui::Id::null()));
184    }
185    fn get_state<T: Clone + Default + Sync + Send + 'static>(self) -> T {
186        self.data_mut(|data| data.get_temp_mut_or_default::<T>(egui::Id::null()).clone())
187    }
188    fn set_state<T: Clone + Default + Sync + Send + 'static>(self, value: T) {
189        self.data_mut(|data| *data.get_temp_mut_or_default::<T>(egui::Id::null()) = value);
190    }
191}
192
193/// Extension trait with helpers for egui responses
194pub trait EguiResponseExt {
195    /// Set this response to focused if nothing else is focused
196    fn focus_by_default(self, ui: &mut egui::Ui) -> egui::Response;
197}
198
199impl EguiResponseExt for egui::Response {
200    fn focus_by_default(self, ui: &mut egui::Ui) -> egui::Response {
201        if ui.ctx().memory(|r| r.focus().is_none()) {
202            ui.ctx().memory_mut(|r| r.request_focus(self.id));
203
204            self
205        } else {
206            self
207        }
208    }
209}