1use std::any::{Any, TypeId};
5use std::marker::PhantomData;
6use yog_sync::atomic::{AtomicBool, Ordering};
7use yog_sync::{Arc, Mutex, OnceLock};
8
9use crate::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
21pub 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 pub fn update(&self, f: impl FnOnce(&mut V) + Send + 'static) {
49 self.shared.queue.lock().push(Box::new(f));
50 }
51
52 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 pub fn id(&self) -> &'static str {
68 self.shared.meta.id
69 }
70
71 pub fn open(&self) {
73 let m = self.shared.meta;
74 yog_api::open_ui(m.id, m.modal, m.pause);
75 }
76
77 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
99pub 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 pub fn close(&mut self) {
113 self.reqs.push(Request::Close);
114 }
115
116 pub fn navigate(&mut self, ui_id: &str) {
118 self.reqs.push(Request::Navigate(ui_id.to_owned()));
119 }
120
121 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 pub fn back(&mut self) {
132 self.reqs.push(Request::Back);
133 }
134
135 pub fn focus(&mut self, id: &str) {
137 self.reqs.push(Request::Focus(id.to_owned()));
138 }
139
140 pub fn handle(&self) -> ScreenHandle<V> {
142 self.handle.clone()
143 }
144
145 pub fn invalidate(&mut self) {
147 self.reqs.push(Request::Invalidate);
148 }
149}
150
151static NAV: Mutex<Vec<&'static str>> = Mutex::new(Vec::new());
154static NAVIGATING: AtomicBool = AtomicBool::new(false);
156static SCREENS: Mutex<Vec<ScreenMeta>> = Mutex::new(Vec::new());
157type 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
173pub(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
183pub(crate) fn screen_closed() {
185 if !NAVIGATING.swap(false, Ordering::SeqCst) {
186 NAV.lock().clear();
187 }
188}
189
190pub 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
199pub struct Screen<W: View>(PhantomData<W>);
201
202impl<W: View> Screen<W> {
203 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 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 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
254fn 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
266fn 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
286pub 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 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}