1use std::any::{Any, TypeId};
5use std::marker::PhantomData;
6use std::sync::atomic::{AtomicBool, Ordering};
7use std::sync::{Arc, Mutex, MutexGuard, 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
15fn lock<T: ?Sized>(m: &Mutex<T>) -> MutexGuard<'_, T> {
16 m.lock().unwrap_or_else(|e| e.into_inner())
17}
18
19pub(crate) struct Shared<V> {
20 meta: ScreenMeta,
21 queue: Mutex<Vec<Job<V>>>,
22 reader: OnceLock<Reader<V>>,
23}
24
25pub struct ScreenHandle<V> {
27 shared: Arc<Shared<V>>,
28}
29
30impl<V> Clone for ScreenHandle<V> {
31 fn clone(&self) -> Self {
32 Self {
33 shared: self.shared.clone(),
34 }
35 }
36}
37
38impl<V: ViewModel> ScreenHandle<V> {
39 pub(crate) fn new(meta: ScreenMeta) -> Self {
40 Self {
41 shared: Arc::new(Shared {
42 meta,
43 queue: Mutex::new(Vec::new()),
44 reader: OnceLock::new(),
45 }),
46 }
47 }
48
49 pub fn update(&self, f: impl FnOnce(&mut V) + Send + 'static) {
53 lock(&self.shared.queue).push(Box::new(f));
54 }
55
56 pub fn read<R>(&self, f: impl FnOnce(&V) -> R) -> Option<R> {
59 let reader = self.shared.reader.get()?;
60 let mut f = Some(f);
61 let mut out = None;
62 reader(&mut |vm| {
63 if let Some(f) = f.take() {
64 out = Some(f(vm));
65 }
66 });
67 out
68 }
69
70 pub fn id(&self) -> &'static str {
72 self.shared.meta.id
73 }
74
75 pub fn open(&self) {
77 let m = self.shared.meta;
78 yog_api::open_ui(m.id, m.modal, m.pause);
79 }
80
81 pub fn close(&self) {
83 yog_api::close_ui(self.shared.meta.id);
84 }
85
86 pub(crate) fn take_queue(&self) -> Vec<Job<V>> {
87 std::mem::take(&mut *lock(&self.shared.queue))
88 }
89
90 pub(crate) fn set_reader(&self, reader: Reader<V>) {
91 let _ = self.shared.reader.set(reader);
92 }
93}
94
95pub(crate) enum Request {
96 Close,
97 Navigate(String),
98 Back,
99 Focus(String),
100 Invalidate,
101}
102
103pub struct Cx<'a, V> {
106 reqs: &'a mut Vec<Request>,
107 handle: &'a ScreenHandle<V>,
108}
109
110impl<'a, V: ViewModel> Cx<'a, V> {
111 pub(crate) fn new(reqs: &'a mut Vec<Request>, handle: &'a ScreenHandle<V>) -> Self {
112 Self { reqs, handle }
113 }
114
115 pub fn close(&mut self) {
117 self.reqs.push(Request::Close);
118 }
119
120 pub fn navigate(&mut self, ui_id: &str) {
122 self.reqs.push(Request::Navigate(ui_id.to_owned()));
123 }
124
125 pub fn navigate_to<W: View>(&mut self, init: impl FnOnce(&mut W::Vm) + Send + 'static) {
128 if let Some(h) = handle_of::<W>() {
129 h.update(init);
130 }
131 self.navigate(W::META.id);
132 }
133
134 pub fn back(&mut self) {
136 self.reqs.push(Request::Back);
137 }
138
139 pub fn focus(&mut self, id: &str) {
141 self.reqs.push(Request::Focus(id.to_owned()));
142 }
143
144 pub fn handle(&self) -> ScreenHandle<V> {
146 self.handle.clone()
147 }
148
149 pub fn invalidate(&mut self) {
151 self.reqs.push(Request::Invalidate);
152 }
153}
154
155static NAV: Mutex<Vec<&'static str>> = Mutex::new(Vec::new());
158static NAVIGATING: AtomicBool = AtomicBool::new(false);
160static SCREENS: Mutex<Vec<ScreenMeta>> = Mutex::new(Vec::new());
161#[allow(clippy::type_complexity)]
162static HANDLES: Mutex<Vec<(TypeId, Box<dyn Any + Send>)>> = Mutex::new(Vec::new());
163
164fn open_id(id: &str) {
165 let meta = lock(&SCREENS).iter().find(|m| m.id == id).copied();
166 let (modal, pause) = meta.map_or((true, false), |m| (m.modal, m.pause));
167 yog_api::open_ui(id, modal, pause);
168}
169
170pub(crate) fn navigate(from: &'static str, to: &str) {
171 lock(&NAV).push(from);
172 NAVIGATING.store(true, Ordering::SeqCst);
173 open_id(to);
174}
175
176pub(crate) fn go_back() -> bool {
178 let Some(prev) = lock(&NAV).pop() else {
179 return false;
180 };
181 NAVIGATING.store(true, Ordering::SeqCst);
182 open_id(prev);
183 true
184}
185
186pub(crate) fn screen_closed() {
188 if !NAVIGATING.swap(false, Ordering::SeqCst) {
189 lock(&NAV).clear();
190 }
191}
192
193pub fn handle_of<W: View>() -> Option<ScreenHandle<W::Vm>> {
195 lock(&HANDLES)
196 .iter()
197 .find(|(t, _)| *t == TypeId::of::<W>())
198 .and_then(|(_, h)| h.downcast_ref::<ScreenHandle<W::Vm>>().cloned())
199}
200
201pub struct Screen<W: View>(PhantomData<W>);
203
204impl<W: View> Screen<W> {
205 pub fn register(registry: &mut yog_api::Registry, vm: W::Vm) -> ScreenHandle<W::Vm> {
208 let handle = ScreenHandle::<W::Vm>::new(W::META);
209 let host = Arc::new(Mutex::new(Host::<W>::new(vm, handle.clone(), true)));
210 let weak = Arc::downgrade(&host);
211 handle.set_reader(Box::new(move |f| {
212 let Some(host) = weak.upgrade() else {
213 return false;
214 };
215 let Ok(h) = host.try_lock() else {
216 return false;
217 };
218 f(&h.vm);
219 true
220 }));
221 lock(&SCREENS).push(W::META);
222 lock(&HANDLES).push((TypeId::of::<W>(), Box::new(handle.clone())));
223 lock(&host).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 lock(&host).every_frame = true;
230 }
231
232 let render = host.clone();
233 registry.on_ui_render(W::META.id, move |gfx| {
234 let mut h = lock(&render);
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| lock(&top).draw_tooltip(gfx));
248 }
249 registry.register_ui(W::META.id, move |_, ev| lock(&host).on_event(ev));
250 handle
251 }
252}
253
254pub fn inventory_of<W: View>() -> Option<yog_api::InventoryDef> {
259 use yog_api::ui::widget::{PLAYER_INVENTORY_ID, SLOT_ID_PREFIX};
260 use yog_api::ui::{UiRoot, Widget};
261
262 fn slots(w: &Widget, out: &mut Vec<(usize, String, Vec<String>)>) {
264 if let Some(id) = w.id.as_deref()
265 && let Some(rest) = id.strip_prefix(SLOT_ID_PREFIX)
266 {
267 let (index, accepts) = rest.split_once('=').unwrap_or((rest, ""));
268 if let Ok(i) = index.parse::<usize>() {
269 let accepts = accepts.split('|').filter(|a| !a.is_empty()).map(str::to_owned).collect();
270 out.push((i, id.to_owned(), accepts));
271 }
272 }
273 for c in &w.children {
274 slots(c, out);
275 }
276 }
277
278 let (_, content, _) = W::build();
279 let mut found = Vec::new();
280 slots(&content, &mut found);
281 let mut ui = UiRoot::new(W::META.id, content);
282 ui.layout(W::META.w, W::META.h);
283 let corner = |id: &str| ui.find_rect(id).map(|r| (r.x + 1.0, r.y + 1.0));
285 let player = corner(PLAYER_INVENTORY_ID);
286 if found.is_empty() && player.is_none() {
287 return None;
288 }
289 let count = found.iter().map(|(i, ..)| i + 1).max().unwrap_or(0);
290 let defaults = yog_api::InventoryDef::default_grid(count);
291 let layout = (0..count)
292 .map(|i| {
293 let slot = found.iter().find(|(j, ..)| *j == i);
294 match slot.and_then(|(_, id, accepts)| Some((corner(id)?, accepts))) {
295 Some(((x, y), accepts)) => yog_api::SlotLayout { x, y, accepts: accepts.clone() },
296 None => {
297 yog_api::warn!("{}: no <Slot index=\"{i}\"> — it goes to the default grid", W::META.id);
298 defaults[i].clone()
299 }
300 }
301 })
302 .collect();
303 let mut def = yog_api::InventoryDef::new(W::META.id, count)
304 .layout(layout)
305 .include_player_inventory(player.is_some())
306 .background_size(W::META.w, W::META.h);
307 if let Some((x, y)) = player {
308 def = def.player_inv_offset(x, y);
309 }
310 Some(def)
311}