Skip to main content

yog_ui/
surface.rs

1//! Surfaces — widgets that draw themselves.
2//!
3//! A [`Surface`] paints its box with any `yog-gfx` call (shaders, buffers,
4//! [`Draw2D`](yog_gfx::draw2d::Draw2D)) and may take the pointer: a
5//! casting grid, a graph, a minimap. The tree refers to one by id
6//! ([`widget::surface`](crate::widget::surface), `.yui` `<Surface
7//! source="mymod:grid"/>`); the code registers the drawing object under
8//! that id with [`register`]. An id nothing is registered under draws
9//! nothing.
10//!
11//! ```ignore
12//! struct Graph;
13//! impl yog_ui::Surface for Graph {
14//!     fn paint(&self, gfx: &GfxContext, rect: Rect) {
15//!         gfx.draw2d().rect(Rect::from_edges(rect.x, rect.y, rect.x + rect.w, rect.y + 1.0), Color(0xFFFFFFFF));
16//!     }
17//! }
18//! yog_ui::surface::register("mymod:graph", yog_sync::Arc::new(Graph));
19//! ```
20
21use std::collections::HashMap;
22use yog_sync::{Arc, Mutex};
23
24use yog_gfx::GfxContext;
25
26use crate::layout::Rect;
27
28/// A pointer event on a surface, in coordinates local to its box (`0,0` =
29/// top-left corner).
30#[derive(Debug, Clone, Copy, PartialEq)]
31pub enum Pointer {
32    /// A button went down over the surface; it gets the drags and the
33    /// release that follow, wherever the cursor goes.
34    Down { x: f32, y: f32 },
35    /// The cursor moved with the button held.
36    Drag { x: f32, y: f32 },
37    /// The button came up.
38    Up { x: f32, y: f32 },
39    /// The cursor moved over the surface, no button held.
40    Move { x: f32, y: f32 },
41    /// The cursor left the surface.
42    Leave,
43}
44
45/// What a surface widget draws and how it takes the pointer — see the
46/// [module docs](self).
47pub trait Surface: Send + Sync {
48    /// Draws into `rect` (screen GUI pixels, the widget's box less its
49    /// padding). Called every frame the widget is visible.
50    fn paint(&self, gfx: &GfxContext, rect: Rect);
51
52    /// A pointer event; `true` = used (a `Down` that returns `false` lets
53    /// the click through to what's under the surface). `size` is the box's
54    /// width and height.
55    fn pointer(&self, ev: Pointer, size: (f32, f32)) -> bool {
56        let _ = (ev, size);
57        false
58    }
59
60    /// The mouse wheel turned over the surface (`dy` > 0 = up); `true` =
61    /// used (scroll containers around it then don't scroll).
62    fn scroll(&self, dy: f32, size: (f32, f32)) -> bool {
63        let _ = (dy, size);
64        false
65    }
66}
67
68type Registry = HashMap<String, Arc<dyn Surface>>;
69
70static SURFACES: Mutex<Option<Registry>> = Mutex::new(None);
71
72/// Makes `surface` what `<Surface source="id"/>` widgets draw (replacing
73/// the one registered before).
74pub fn register(id: impl Into<String>, surface: Arc<dyn Surface>) {
75    SURFACES
76        .lock()
77        .get_or_insert_with(HashMap::new)
78        .insert(id.into(), surface);
79}
80
81/// Drops the surface registered under `id`.
82pub fn unregister(id: &str) {
83    if let Some(map) = SURFACES.lock().as_mut() {
84        map.remove(id);
85    }
86}
87
88/// The surface registered under `id`.
89pub fn get(id: &str) -> Option<Arc<dyn Surface>> {
90    SURFACES.lock().as_ref()?.get(id).cloned()
91}
92
93#[cfg(test)]
94mod tests {
95    use super::*;
96    use yog_sync::atomic::{AtomicU32, Ordering};
97
98    struct Counter(AtomicU32);
99    impl Surface for Counter {
100        fn paint(&self, _: &GfxContext, _: Rect) {}
101        fn pointer(&self, _: Pointer, _: (f32, f32)) -> bool {
102            self.0.fetch_add(1, Ordering::Relaxed);
103            true
104        }
105    }
106
107    #[test]
108    fn registry_finds_by_id() {
109        assert!(get("test:none").is_none());
110        let c = Arc::new(Counter(AtomicU32::new(0)));
111        register("test:counter", c.clone());
112        let s = get("test:counter").unwrap();
113        assert!(s.pointer(Pointer::Leave, (1.0, 1.0)));
114        assert_eq!(c.0.load(Ordering::Relaxed), 1);
115        unregister("test:counter");
116        assert!(get("test:counter").is_none());
117    }
118}