renderling/ui/
cpu.rs

1//! CPU part of ui.
2
3use core::sync::atomic::AtomicBool;
4use std::sync::{Arc, RwLock};
5
6use crate::{
7    atlas::{shader::AtlasTextureDescriptor, AtlasTexture, TextureAddressMode, TextureModes},
8    camera::Camera,
9    context::Context,
10    stage::Stage,
11    transform::NestedTransform,
12};
13use crabslab::Id;
14use glam::{Quat, UVec2, Vec2, Vec3Swizzles, Vec4};
15use glyph_brush::ab_glyph;
16use rustc_hash::FxHashMap;
17use snafu::{prelude::*, ResultExt};
18
19pub use glyph_brush::FontId;
20
21mod path;
22pub use path::*;
23
24mod text;
25pub use text::*;
26
27pub mod prelude {
28    //! A prelude for user interface development, meant to be glob-imported.
29
30    #[cfg(cpu)]
31    pub extern crate craballoc;
32    pub extern crate glam;
33
34    #[cfg(cpu)]
35    pub use craballoc::prelude::*;
36    pub use crabslab::{Array, Id};
37
38    #[cfg(cpu)]
39    pub use crate::context::*;
40
41    pub use super::*;
42}
43
44#[derive(Debug, Snafu)]
45pub enum UiError {
46    #[snafu(display("{source}"))]
47    Loading {
48        source: loading_bytes::LoadingBytesError,
49    },
50
51    #[snafu(display("{source}"))]
52    InvalidFont { source: ab_glyph::InvalidFont },
53
54    #[snafu(display("{source}"))]
55    Image { source: image::ImageError },
56
57    #[snafu(display("{source}"))]
58    Stage { source: crate::stage::StageError },
59}
60
61/// An image identifier.
62///
63/// This locates the image within a [`Ui`].
64///
65/// `ImageId` can be created with [`Ui::load_image`].
66#[repr(transparent)]
67#[derive(Clone, Copy, Debug)]
68pub struct ImageId(Id<AtlasTextureDescriptor>);
69
70/// A two dimensional transformation.
71///
72/// Clones of `UiTransform` all point to the same data.
73#[derive(Clone, Debug)]
74pub struct UiTransform {
75    should_reorder: Arc<AtomicBool>,
76    transform: NestedTransform,
77}
78
79impl UiTransform {
80    fn mark_should_reorder(&self) {
81        self.should_reorder
82            .store(true, std::sync::atomic::Ordering::Relaxed);
83    }
84
85    pub fn set_translation(&self, t: Vec2) {
86        self.mark_should_reorder();
87        self.transform.modify_local_translation(|a| {
88            a.x = t.x;
89            a.y = t.y;
90        });
91    }
92
93    pub fn get_translation(&self) -> Vec2 {
94        self.transform.local_translation().xy()
95    }
96
97    pub fn set_rotation(&self, radians: f32) {
98        self.mark_should_reorder();
99        let rotation = Quat::from_rotation_z(radians);
100        // TODO: check to see if *= rotation makes sense here
101        self.transform.modify_local_rotation(|t| {
102            *t *= rotation;
103        });
104    }
105
106    pub fn get_rotation(&self) -> f32 {
107        self.transform
108            .local_rotation()
109            .to_euler(glam::EulerRot::XYZ)
110            .2
111    }
112
113    pub fn set_z(&self, z: f32) {
114        self.mark_should_reorder();
115        self.transform.modify_local_translation(|t| {
116            t.z = z;
117        });
118    }
119
120    pub fn get_z(&self) -> f32 {
121        self.transform.local_translation().z
122    }
123}
124
125#[derive(Clone)]
126#[repr(transparent)]
127pub struct UiImage(AtlasTexture);
128
129/// A 2d user interface renderer.
130///
131/// Clones of `Ui` all point to the same data.
132#[derive(Clone)]
133pub struct Ui {
134    camera: Camera,
135    stage: Stage,
136    should_reorder: Arc<AtomicBool>,
137    images: Arc<RwLock<FxHashMap<Id<AtlasTextureDescriptor>, UiImage>>>,
138    fonts: Arc<RwLock<Vec<FontArc>>>,
139    default_stroke_options: Arc<RwLock<StrokeOptions>>,
140    default_fill_options: Arc<RwLock<FillOptions>>,
141}
142
143impl Ui {
144    pub fn new(ctx: &Context) -> Self {
145        let UVec2 { x, y } = ctx.get_size();
146        let stage = ctx
147            .new_stage()
148            .with_background_color(Vec4::ONE)
149            .with_lighting(false)
150            .with_bloom(false)
151            .with_msaa_sample_count(4)
152            .with_frustum_culling(false);
153        let (proj, view) = crate::camera::default_ortho2d(x as f32, y as f32);
154        let camera = stage.new_camera().with_projection_and_view(proj, view);
155        Ui {
156            camera,
157            stage,
158            should_reorder: AtomicBool::new(true).into(),
159            images: Default::default(),
160            fonts: Default::default(),
161            default_stroke_options: Default::default(),
162            default_fill_options: Default::default(),
163        }
164    }
165
166    pub fn set_clear_color_attachments(&self, should_clear: bool) {
167        self.stage.set_clear_color_attachments(should_clear);
168    }
169
170    pub fn with_clear_color_attachments(self, should_clear: bool) -> Self {
171        self.set_clear_color_attachments(should_clear);
172        self
173    }
174
175    pub fn set_clear_depth_attachments(&self, should_clear: bool) {
176        self.stage.set_clear_depth_attachments(should_clear);
177    }
178
179    pub fn with_clear_depth_attachments(self, should_clear: bool) -> Self {
180        self.set_clear_depth_attachments(should_clear);
181        self
182    }
183
184    pub fn set_background_color(&self, color: impl Into<Vec4>) -> &Self {
185        self.stage.set_background_color(color);
186        self
187    }
188
189    pub fn with_background_color(self, color: impl Into<Vec4>) -> Self {
190        self.set_background_color(color);
191        self
192    }
193
194    pub fn set_antialiasing(&self, antialiasing_is_on: bool) -> &Self {
195        let sample_count = if antialiasing_is_on { 4 } else { 1 };
196        self.stage.set_msaa_sample_count(sample_count);
197        self
198    }
199
200    pub fn with_antialiasing(self, antialiasing_is_on: bool) -> Self {
201        self.set_antialiasing(antialiasing_is_on);
202        self
203    }
204
205    pub fn set_default_stroke_options(&self, options: StrokeOptions) -> &Self {
206        *self.default_stroke_options.write().unwrap() = options;
207        self
208    }
209
210    pub fn with_default_stroke_options(self, options: StrokeOptions) -> Self {
211        self.set_default_stroke_options(options);
212        self
213    }
214
215    pub fn set_default_fill_options(&self, options: FillOptions) -> &Self {
216        *self.default_fill_options.write().unwrap() = options;
217        self
218    }
219
220    pub fn with_default_fill_options(self, options: FillOptions) -> Self {
221        self.set_default_fill_options(options);
222        self
223    }
224
225    fn new_transform(&self) -> UiTransform {
226        self.mark_should_reorder();
227        let transform = self.stage.new_nested_transform();
228        UiTransform {
229            transform,
230            should_reorder: self.should_reorder.clone(),
231        }
232    }
233
234    fn mark_should_reorder(&self) {
235        self.should_reorder
236            .store(true, std::sync::atomic::Ordering::Relaxed)
237    }
238
239    pub fn path_builder(&self) -> UiPathBuilder {
240        self.mark_should_reorder();
241        UiPathBuilder::new(self)
242    }
243
244    /// Remove the `path` from the [`Ui`].
245    ///
246    /// The given `path` must have been created with this [`Ui`], otherwise this function is
247    /// a noop.
248    pub fn remove_path(&self, path: &UiPath) {
249        self.stage.remove_primitive(&path.primitive);
250    }
251
252    pub fn text_builder(&self) -> UiTextBuilder {
253        self.mark_should_reorder();
254        UiTextBuilder::new(self)
255    }
256
257    /// Remove the text from the [`Ui`].
258    ///
259    /// The given `text` must have been created with this [`Ui`], otherwise this function is
260    /// a noop.
261    pub fn remove_text(&self, text: &UiText) {
262        self.stage.remove_primitive(&text.renderlet);
263    }
264
265    pub async fn load_font(&self, path: impl AsRef<str>) -> Result<FontId, UiError> {
266        let path_s = path.as_ref();
267        let bytes = loading_bytes::load(path_s).await.context(LoadingSnafu)?;
268        let font = FontArc::try_from_vec(bytes).context(InvalidFontSnafu)?;
269        Ok(self.add_font(font))
270    }
271
272    pub fn add_font(&self, font: FontArc) -> FontId {
273        // UNWRAP: panic on purpose
274        let mut fonts = self.fonts.write().unwrap();
275        let id = fonts.len();
276        fonts.push(font);
277        FontId(id)
278    }
279
280    pub fn get_fonts(&self) -> Vec<FontArc> {
281        // UNWRAP: panic on purpose
282        self.fonts.read().unwrap().clone()
283    }
284
285    pub fn get_camera(&self) -> &Camera {
286        &self.camera
287    }
288
289    pub async fn load_image(&self, path: impl AsRef<str>) -> Result<ImageId, UiError> {
290        let path_s = path.as_ref();
291        let bytes = loading_bytes::load(path_s).await.context(LoadingSnafu)?;
292        let img = image::load_from_memory_with_format(
293            bytes.as_slice(),
294            image::ImageFormat::from_path(path_s).context(ImageSnafu)?,
295        )
296        .context(ImageSnafu)?;
297        let entry = self
298            .stage
299            .add_images(Some(img))
300            .context(StageSnafu)?
301            .pop()
302            .unwrap();
303        entry.set_modes(TextureModes {
304            s: TextureAddressMode::Repeat,
305            t: TextureAddressMode::Repeat,
306        });
307        let mut guard = self.images.write().unwrap();
308        let id = entry.id();
309        guard.insert(id, UiImage(entry));
310        Ok(ImageId(id))
311    }
312
313    /// Remove an image previously loaded with [`Ui::load_image`].
314    pub fn remove_image(&self, image_id: &ImageId) -> Option<UiImage> {
315        self.images.write().unwrap().remove(&image_id.0)
316    }
317
318    fn reorder_renderlets(&self) {
319        self.stage.sort_primitive(|a, b| {
320            let za = a
321                .transform()
322                .as_ref()
323                .map(|t| t.translation().z)
324                .unwrap_or_default();
325            let zb = b
326                .transform()
327                .as_ref()
328                .map(|t| t.translation().z)
329                .unwrap_or_default();
330            za.total_cmp(&zb)
331        });
332    }
333
334    pub fn render(&self, view: &wgpu::TextureView) {
335        if self
336            .should_reorder
337            .swap(false, std::sync::atomic::Ordering::Relaxed)
338        {
339            self.reorder_renderlets();
340        }
341        self.stage.render(view);
342    }
343}
344
345#[cfg(test)]
346pub(crate) mod test {
347    use crate::{color::rgb_hex_color, glam::Vec4};
348
349    pub struct Colors<const N: usize>(std::iter::Cycle<std::array::IntoIter<Vec4, N>>);
350
351    pub fn cute_beach_palette() -> [Vec4; 4] {
352        [
353            rgb_hex_color(0x6DC5D1),
354            rgb_hex_color(0xFDE49E),
355            rgb_hex_color(0xFEB941),
356            rgb_hex_color(0xDD761C),
357        ]
358    }
359
360    impl<const N: usize> Colors<N> {
361        pub fn from_array(colors: [Vec4; N]) -> Self {
362            Colors(colors.into_iter().cycle())
363        }
364
365        pub fn next_color(&mut self) -> Vec4 {
366            self.0.next().unwrap()
367        }
368    }
369}