Skip to main content

yog_kit/screens/
screen.rs

1//! Registering a screen with the game, handles to it, the command context
2//! and navigation between screens.
3
4use std::any::{Any, TypeId};
5use std::marker::PhantomData;
6use yog_sync::atomic::{AtomicBool, Ordering};
7use yog_sync::{Arc, Mutex, OnceLock};
8
9use crate::screens::host::Host;
10use crate::{ScreenMeta, View, ViewModel};
11
12type Job<V> = Box<dyn FnOnce(&mut V) + Send>;
13type Reader<V> = Box<dyn Fn(&mut dyn FnMut(&V)) -> bool + Send + Sync>;
14
15pub(crate) struct Shared<V> {
16    meta: ScreenMeta,
17    queue: Mutex<Vec<Job<V>>>,
18    reader: OnceLock<Reader<V>>,
19}
20
21/// A cloneable, thread-safe handle to a registered screen's view model.
22pub struct ScreenHandle<V> {
23    shared: Arc<Shared<V>>,
24}
25
26impl<V> Clone for ScreenHandle<V> {
27    fn clone(&self) -> Self {
28        Self {
29            shared: self.shared.clone(),
30        }
31    }
32}
33
34impl<V: ViewModel> ScreenHandle<V> {
35    pub(crate) fn new(meta: ScreenMeta) -> Self {
36        Self {
37            shared: Arc::new(Shared {
38                meta,
39                queue: Mutex::new(Vec::new()),
40                reader: OnceLock::new(),
41            }),
42        }
43    }
44
45    /// Changes the view model from any thread (a network handler, a
46    /// command, ...). Queued: runs on the render thread before the next
47    /// frame or event, then the bindings update. Never blocks.
48    pub fn update(&self, f: impl FnOnce(&mut V) + Send + 'static) {
49        self.shared.queue.lock().push(Box::new(f));
50    }
51
52    /// Reads the view model — `None` while the screen is busy (rendering or
53    /// handling input, e.g. when called from one of its own commands).
54    pub fn read<R>(&self, f: impl FnOnce(&V) -> R) -> Option<R> {
55        let reader = self.shared.reader.get()?;
56        let mut f = Some(f);
57        let mut out = None;
58        reader(&mut |vm| {
59            if let Some(f) = f.take() {
60                out = Some(f(vm));
61            }
62        });
63        out
64    }
65
66    /// The screen's UI id.
67    pub fn id(&self) -> &'static str {
68        self.shared.meta.id
69    }
70
71    /// Opens the screen.
72    pub fn open(&self) {
73        let m = self.shared.meta;
74        yog_api::open_ui(m.id, m.modal, m.pause);
75    }
76
77    /// Closes the screen if it's open.
78    pub fn close(&self) {
79        yog_api::close_ui(self.shared.meta.id);
80    }
81
82    pub(crate) fn take_queue(&self) -> Vec<Job<V>> {
83        std::mem::take(&mut *self.shared.queue.lock())
84    }
85
86    pub(crate) fn set_reader(&self, reader: Reader<V>) {
87        let _ = self.shared.reader.set(reader);
88    }
89}
90
91pub(crate) enum Request {
92    Close,
93    Navigate(String),
94    Back,
95    Focus(String),
96    Invalidate,
97}
98
99/// What a command or view-model hook can ask of its screen. Requests run
100/// right after the call returns.
101pub struct Cx<'a, V> {
102    reqs: &'a mut Vec<Request>,
103    handle: &'a ScreenHandle<V>,
104}
105
106impl<'a, V: ViewModel> Cx<'a, V> {
107    pub(crate) fn new(reqs: &'a mut Vec<Request>, handle: &'a ScreenHandle<V>) -> Self {
108        Self { reqs, handle }
109    }
110
111    /// Closes this screen.
112    pub fn close(&mut self) {
113        self.reqs.push(Request::Close);
114    }
115
116    /// Opens the screen `ui_id`; Esc there comes back here.
117    pub fn navigate(&mut self, ui_id: &str) {
118        self.reqs.push(Request::Navigate(ui_id.to_owned()));
119    }
120
121    /// Opens the yog-kit screen `W` of this mod after running `init` on its
122    /// view model; Esc there comes back here.
123    pub fn navigate_to<W: View>(&mut self, init: impl FnOnce(&mut W::Vm) + Send + 'static) {
124        if let Some(h) = handle_of::<W>() {
125            h.update(init);
126        }
127        self.navigate(W::META.id);
128    }
129
130    /// Back to the screen this one was navigated from, else closes it.
131    pub fn back(&mut self) {
132        self.reqs.push(Request::Back);
133    }
134
135    /// Moves the keyboard focus to the widget with `id`.
136    pub fn focus(&mut self, id: &str) {
137        self.reqs.push(Request::Focus(id.to_owned()));
138    }
139
140    /// A handle to this screen (keep it for updates from other threads).
141    pub fn handle(&self) -> ScreenHandle<V> {
142        self.handle.clone()
143    }
144
145    /// Re-evaluates the bindings (they already are after every command).
146    pub fn invalidate(&mut self) {
147        self.reqs.push(Request::Invalidate);
148    }
149}
150
151// ── navigation (per mod: each mod links its own yog-kit) ─────────────────────
152
153static NAV: Mutex<Vec<&'static str>> = Mutex::new(Vec::new());
154/// A navigation is closing the current screen — keep the stack.
155static NAVIGATING: AtomicBool = AtomicBool::new(false);
156static SCREENS: Mutex<Vec<ScreenMeta>> = Mutex::new(Vec::new());
157/// Each screen type's handle, by its type.
158type Handles = Vec<(TypeId, Box<dyn Any + Send>)>;
159static HANDLES: Mutex<Handles> = Mutex::new(Vec::new());
160
161fn open_id(id: &str) {
162    let meta = SCREENS.lock().iter().find(|m| m.id == id).copied();
163    let (modal, pause) = meta.map_or((true, false), |m| (m.modal, m.pause));
164    yog_api::open_ui(id, modal, pause);
165}
166
167pub(crate) fn navigate(from: &'static str, to: &str) {
168    NAV.lock().push(from);
169    NAVIGATING.store(true, Ordering::SeqCst);
170    open_id(to);
171}
172
173/// Opens the previous screen; `false` when there's none.
174pub(crate) fn go_back() -> bool {
175    let Some(prev) = NAV.lock().pop() else {
176        return false;
177    };
178    NAVIGATING.store(true, Ordering::SeqCst);
179    open_id(prev);
180    true
181}
182
183/// A screen closed: unless it's a navigation, the history is over.
184pub(crate) fn screen_closed() {
185    if !NAVIGATING.swap(false, Ordering::SeqCst) {
186        NAV.lock().clear();
187    }
188}
189
190/// The handle of the registered screen `W`.
191pub fn handle_of<W: View>() -> Option<ScreenHandle<W::Vm>> {
192    HANDLES
193        .lock()
194        .iter()
195        .find(|(t, _)| *t == TypeId::of::<W>())
196        .and_then(|(_, h)| h.downcast_ref::<ScreenHandle<W::Vm>>().cloned())
197}
198
199/// Hosts the compiled view `W` as a Yog screen.
200pub struct Screen<W: View>(PhantomData<W>);
201
202impl<W: View> Screen<W> {
203    /// Registers `W` (rendering + input) with the view model `vm`; called
204    /// by the generated `register()`.
205    pub fn register(registry: &mut yog_api::Registry, vm: W::Vm) -> ScreenHandle<W::Vm> {
206        let handle = ScreenHandle::<W::Vm>::new(W::META);
207        let host = Arc::new(Mutex::new(Host::<W>::new(vm, handle.clone(), true)));
208        let weak = Arc::downgrade(&host);
209        handle.set_reader(Box::new(move |f| {
210            let Some(host) = weak.upgrade() else {
211                return false;
212            };
213            let Some(h) = host.try_lock() else {
214                return false;
215            };
216            f(&h.vm);
217            true
218        }));
219        SCREENS.lock().push(W::META);
220        HANDLES
221            .lock()
222            .push((TypeId::of::<W>(), Box::new(handle.clone())));
223        host.lock().every_frame = W::META.live;
224        // Opened by the game with the block it belongs to, not by `open()`.
225        let inventory = inventory_of::<W>();
226        let overlay = inventory.is_some();
227        if let Some(def) = inventory {
228            registry.register_inventory(def);
229            host.lock().every_frame = true;
230        }
231
232        let render = host.clone();
233        registry.on_ui_render(W::META.id, move |gfx| {
234            let mut h = render.lock();
235            let (w, hh) = gfx.screen_size();
236            h.frame(w as f32, hh as f32);
237            h.ui.render(gfx);
238            if !overlay {
239                h.draw_tooltip(gfx);
240            }
241        });
242        // An inventory screen's game-drawn items come after its markup: the
243        // host draws `<id>#overlay` after them, for what goes on top.
244        if overlay {
245            let top = host.clone();
246            let id: &'static str = Box::leak(format!("{}#overlay", W::META.id).into_boxed_str());
247            registry.on_ui_render(id, move |gfx| top.lock().draw_tooltip(gfx));
248        }
249        registry.register_ui(W::META.id, move |_, ev| host.lock().on_event(ev));
250        handle
251    }
252}
253
254/// Each slot in `w`: (index, its id, what it accepts).
255fn slots(w: &yog_api::ui::Widget, out: &mut Vec<(usize, String, Vec<String>)>) {
256    if let Some(id) = w.id.as_deref()
257        && let Some((i, accepts)) = yog_api::ui::inventory_ids::read_slot(id)
258    {
259        out.push((i, id.to_owned(), accepts));
260    }
261    for c in &w.children {
262        slots(c, out);
263    }
264}
265
266/// The machine a `<RecipeArea>` made of it, if any (`widget::machine`).
267fn machine(w: &yog_api::ui::Widget) -> Option<yog_api::inventory::Machine> {
268    if let Some(m) =
269        w.id.as_deref()
270            .and_then(yog_api::ui::inventory_ids::MachineId::read)
271    {
272        let places = m
273            .places
274            .into_iter()
275            .map(|(slot, index, inv)| yog_api::inventory::MachinePlace { slot, index, inv })
276            .collect();
277        return Some(yog_api::inventory::Machine {
278            kind: m.kind,
279            places,
280            result: m.result,
281        });
282    }
283    w.children.iter().find_map(machine)
284}
285
286/// The inventory screen `W`'s markup lays out — where its `<Slot>`s and
287/// `<PlayerInventory>` land at the design size — or `None` for a plain
288/// screen. Its id is the screen's: a block opens it with
289/// `BlockDef::inventory(ID)`.
290pub fn inventory_of<W: View>() -> Option<yog_api::InventoryDef> {
291    use yog_api::ui::UiRoot;
292    use yog_api::ui::widget::PLAYER_INVENTORY;
293
294    let (_, content, _) = W::build();
295    let mut found = Vec::new();
296    slots(&content, &mut found);
297    let made = machine(&content);
298    let mut ui = UiRoot::new(W::META.id, content);
299    ui.layout(W::META.w, W::META.h);
300    // A real slot's position is its item's corner, inside the 18px frame.
301    let corner = |id: &str| ui.find_rect(id).map(|r| (r.x + 1.0, r.y + 1.0));
302    let player = corner(PLAYER_INVENTORY);
303    if found.is_empty() && player.is_none() {
304        return None;
305    }
306    let count = found.iter().map(|(i, ..)| i + 1).max().unwrap_or(0);
307    let defaults = yog_api::InventoryDef::default_grid(count);
308    let layout = (0..count)
309        .map(|i| {
310            let slot = found.iter().find(|(j, ..)| *j == i);
311            match slot.and_then(|(_, id, accepts)| Some((corner(id)?, accepts))) {
312                Some(((x, y), accepts)) => yog_api::SlotLayout {
313                    x,
314                    y,
315                    accepts: accepts.clone(),
316                },
317                None => {
318                    yog_api::warn!(
319                        "{}: no <Slot index=\"{i}\"> — it goes to the default grid",
320                        W::META.id
321                    );
322                    defaults[i].clone()
323                }
324            }
325        })
326        .collect();
327    let mut def = yog_api::InventoryDef::new(W::META.id, count)
328        .layout(layout)
329        .include_player_inventory(player.is_some())
330        .background_size(W::META.w, W::META.h);
331    if let Some((x, y)) = player {
332        def = def.player_inv_offset(x, y);
333    }
334    if let Some(m) = made {
335        def = def.machine(m);
336    }
337    Some(def)
338}