1use alloc::boxed::Box;
2use alloc::string::{String, ToString as _};
3use alloc::sync::Arc;
4use core::fmt;
5use std::sync::Mutex;
6
7use flume::TryRecvError;
8
9use all_is_cubes::arcstr::ArcStr;
10use all_is_cubes::character::{Character, Cursor};
11use all_is_cubes::inv::{EphemeralOpaque, Tool, ToolError, ToolInput};
12use all_is_cubes::listen::{self, Notifier};
13use all_is_cubes::space::Space;
14use all_is_cubes::time;
15use all_is_cubes::transaction::{self, Transaction};
16use all_is_cubes::universe::{
17 Handle, ReadTicket, StrongHandle, Universe, UniverseStepInfo, UniverseTransaction,
18};
19use all_is_cubes_render::camera::{FogOption, GraphicsOptions, UiViewState, Viewport};
20
21use crate::apps::{
22 ControlMessage, FullscreenSetter, FullscreenState, QuitCancelled, QuitFn, QuitResult,
23};
24use crate::ui_content::hud::{HudBlocks, HudInputs};
25use crate::ui_content::{notification, pages};
26use crate::vui::widgets::TooltipState;
27use crate::vui::{self, PageInst, UiSize};
28
29#[derive(Clone)]
36pub(crate) struct UiTargets {
37 pub(crate) mouselook_mode: listen::DynSource<bool>,
38
39 pub(crate) character_source: listen::DynSource<Option<StrongHandle<Character>>>,
41
42 pub(crate) paused: listen::DynSource<bool>,
43
44 pub(crate) graphics_options: listen::DynSource<Arc<GraphicsOptions>>,
45
46 pub(crate) app_control_channel: flume::Sender<ControlMessage>,
47
48 pub(crate) viewport_source: listen::DynSource<Viewport>,
49
50 pub(crate) fullscreen_mode: listen::DynSource<FullscreenState>,
51 pub(crate) set_fullscreen: FullscreenSetter,
52
53 pub(crate) quit: Option<QuitFn>,
54
55 pub(crate) custom_commands: listen::DynSource<Arc<[Command]>>,
56}
57
58#[allow(missing_docs)]
60#[derive(Clone)]
61#[allow(clippy::exhaustive_structs)]
62pub struct Command {
63 pub label: ArcStr,
64 pub command:
68 Arc<dyn Fn() -> Result<(), Box<dyn core::error::Error + Send + Sync>> + Send + Sync>,
69}
70
71#[derive(Debug)] pub(crate) struct Vui {
80 universe: Universe,
82
83 current_view: listen::Cell<Arc<UiViewState>>,
86
87 current_focus_on_ui: bool,
92
93 state: listen::Cell<Arc<VuiPageState>>,
96
97 changed_graphics_options: listen::Flag,
99 ui_graphics_options: listen::Cell<Arc<GraphicsOptions>>,
101
102 changed_viewport: listen::Flag,
103 last_ui_size: UiSize,
105 hud_inputs: HudInputs,
106
107 hud_page: PageInst,
108 paused_page: PageInst,
109 about_page: PageInst,
110 progress_page: PageInst,
111 options_page: PageInst,
112 dump_page: PageInst,
114
115 control_channel: flume::Receiver<VuiMessage>,
122 changed_character: listen::Flag,
123 changed_custom_commands: listen::Flag,
124 tooltip_state: Arc<Mutex<TooltipState>>,
125 cue_channel: CueNotifier,
127
128 notif_hub: notification::Hub,
129}
130
131impl Vui {
132 pub(crate) async fn new(params: UiTargets) -> Self {
138 let UiTargets {
139 viewport_source, ..
140 } = ¶ms;
141
142 let mut universe = Universe::new();
143
144 let mut content_txn = UniverseTransaction::default();
145 let hud_blocks = Arc::new(
147 HudBlocks::new(
148 universe.read_ticket(),
149 &mut content_txn,
150 all_is_cubes::util::YieldProgressBuilder::new().build(),
151 )
152 .await,
153 );
154 content_txn.execute(&mut universe, (), &mut transaction::no_outputs).unwrap();
155
156 let (control_send, control_recv) = flume::bounded(100);
157 let state = listen::Cell::new(Arc::new(VuiPageState::Hud));
158
159 let tooltip_state = Arc::<Mutex<TooltipState>>::default();
160 let cue_channel: CueNotifier = Arc::new(Notifier::new());
161 let notif_hub = notification::Hub::new();
162
163 let changed_graphics_options = listen::Flag::listening(false, ¶ms.graphics_options);
164 let ui_graphics_options = listen::Cell::new(Arc::new(Self::graphics_options(
165 (*params.graphics_options.get()).clone(),
166 )));
167
168 let changed_custom_commands = listen::Flag::listening(false, ¶ms.custom_commands);
169 let changed_viewport = listen::Flag::listening(false, &viewport_source);
170 let ui_size = UiSize::new(viewport_source.get());
171 let hud_inputs = HudInputs {
172 base: params.clone(),
173 hud_blocks,
174 cue_channel: cue_channel.clone(),
175 vui_control_channel: control_send,
176 page_state: state.as_source(),
177 };
178 let hud_page =
179 super::hud::new_hud_page(universe.read_ticket(), &hud_inputs, tooltip_state.clone());
180
181 let paused_page = pages::new_paused_page(&mut universe, &hud_inputs).unwrap();
182 let options_page = pages::new_options_widget_tree(universe.read_ticket(), &hud_inputs);
183 let about_page = pages::new_about_page(&mut universe, &hud_inputs).unwrap();
184 let progress_page =
185 pages::new_progress_page(&hud_inputs.hud_blocks.widget_theme, ¬if_hub);
186
187 let mut new_self = Self {
188 universe: *universe,
189
190 current_view: listen::Cell::new(Arc::new(UiViewState::default())),
191 current_focus_on_ui: false,
192 state: listen::Cell::new(Arc::new(VuiPageState::Hud)),
193
194 changed_graphics_options,
195 ui_graphics_options,
196
197 changed_viewport,
198 last_ui_size: ui_size,
199 hud_inputs,
200
201 hud_page: PageInst::new(hud_page),
202 paused_page: PageInst::new(paused_page),
203 options_page: PageInst::new(options_page),
204 about_page: PageInst::new(about_page),
205 dump_page: PageInst::new(vui::Page::empty()),
206 progress_page: PageInst::new(progress_page),
207
208 control_channel: control_recv,
209 changed_character: listen::Flag::listening(false, ¶ms.character_source),
210 changed_custom_commands,
211 tooltip_state,
212 cue_channel,
213 notif_hub,
214 };
215 new_self.compute_view_state();
216 new_self
217 }
218
219 pub fn view(&self) -> listen::DynSource<Arc<UiViewState>> {
222 self.current_view.as_source()
223 }
224
225 pub fn read_ticket(&self) -> ReadTicket<'_> {
227 self.universe.read_ticket()
228 }
229
230 pub(crate) fn should_focus_on_ui(&self) -> bool {
236 self.current_focus_on_ui
237 }
238
239 pub(crate) fn set_state(&mut self, state: impl Into<Arc<VuiPageState>>) {
240 self.state.set(state.into());
241
242 if let VuiPageState::Dump {
245 previous: _,
246 content,
247 } = &*self.state.get()
248 {
249 let content: vui::Page = match content.try_ref() {
250 Some(page) => (*page).clone(),
251 None => vui::Page::empty(),
252 };
253 self.dump_page = PageInst::new(content);
254 }
255
256 self.compute_view_state();
257 }
258
259 fn compute_view_state(&mut self) {
261 let size = self.last_ui_size;
262 let universe = &mut self.universe;
263
264 let next_page: &mut PageInst = match &*self.state.get() {
265 VuiPageState::Hud => &mut self.hud_page,
266 VuiPageState::Paused => {
267 if self.changed_custom_commands.get_and_clear() {
268 self.paused_page =
271 PageInst::new(pages::new_paused_page(universe, &self.hud_inputs).unwrap());
272 }
273 &mut self.paused_page
274 }
275 VuiPageState::Options => &mut self.options_page,
276 VuiPageState::AboutText => &mut self.about_page,
277 VuiPageState::Progress => &mut self.progress_page,
278
279 VuiPageState::Dump {
281 previous: _,
282 content: _,
283 } => &mut self.dump_page,
284 };
285 let next_space: Handle<Space> = next_page.get_or_create_space(size, universe);
286 let page_layout = next_page.page().layout;
287 let graphics_options = self.ui_graphics_options.get();
288
289 let new_view_state = UiViewState {
290 view_transform: page_layout.view_transform(
291 &next_space.read(universe.read_ticket()).unwrap(), graphics_options.fov_y.into_inner(),
293 ),
294 space: Some(next_space),
295 graphics_options,
296 };
297
298 if new_view_state != *self.current_view.get() {
299 self.current_view.set(Arc::new(new_view_state));
300 self.current_focus_on_ui = next_page.page().focus_on_ui;
301 log::trace!(
302 "UI switched to {:?} ({:?})",
303 self.current_view.get().space,
304 self.state.get()
305 );
306 }
307 }
308
309 fn graphics_options(mut options: GraphicsOptions) -> GraphicsOptions {
311 options.fov_y = 30u8.into();
313
314 options.fog = FogOption::None;
316
317 options.view_distance = 100u8.into();
320
321 options.debug_chunk_boxes = false;
323 options.debug_pixel_cost = false;
324
325 options
326 }
327
328 pub fn step(
331 &mut self,
332 tick: time::Tick,
333 deadline: time::Deadline,
334 world_read_ticket: ReadTicket<'_>,
335 ) -> UniverseStepInfo {
336 self.step_pre_sync(world_read_ticket);
337 self.universe.step(tick.paused(), deadline)
338 }
339
340 #[inline(never)]
342 fn step_pre_sync(&mut self, world_read_ticket: ReadTicket<'_>) {
343 let mut anything_changed = false;
344
345 if self.changed_graphics_options.get_and_clear() {
346 anything_changed = true;
347 self.ui_graphics_options.set_if_unequal(Arc::new(Self::graphics_options(
348 (*self.hud_inputs.graphics_options.get()).clone(),
349 )));
350 }
351
352 if self.changed_character.get_and_clear()
354 && let Some(character_handle) = self.hud_inputs.character_source.get()
355 {
356 TooltipState::bind_to_character(
357 world_read_ticket,
358 &self.tooltip_state,
359 character_handle,
360 );
361 }
362
363 loop {
365 match self.control_channel.try_recv() {
366 Ok(msg) => match msg {
367 VuiMessage::Back => {
368 self.back();
369 }
370 VuiMessage::Open(page) => {
371 self.set_state(page);
373 }
374 },
375 Err(TryRecvError::Empty) => break,
376 Err(TryRecvError::Disconnected) => {
377 }
379 }
380 }
381
382 if self.changed_viewport.get_and_clear() {
383 let new_viewport = self.hud_inputs.viewport_source.get();
384 let new_size = UiSize::new(new_viewport);
385 if new_size != self.last_ui_size {
386 anything_changed = true;
387 self.last_ui_size = new_size;
388 self.current_view.set(Arc::new(UiViewState::default())); }
390 }
391
392 self.notif_hub.update();
394
395 if let Some(new_state) = self.choose_new_page_state(&self.state.get()) {
397 self.set_state(new_state);
398 } else if anything_changed {
399 self.compute_view_state();
402 }
403
404 if let Some(space_handle) = self.view().get().space.as_ref() {
405 if let Ok(space) = space_handle.read(self.universe.read_ticket()) {
406 vui::synchronize_widgets(world_read_ticket, self.universe.read_ticket(), &space);
407 } else {
408 log::error!("failed to synchronize widgets");
409 }
410 }
411 }
412
413 pub fn show_modal_message(&mut self, message: ArcStr) {
414 let content =
415 EphemeralOpaque::from(Arc::new(pages::new_message_page(message, &self.hud_inputs)));
416 self.set_state(VuiPageState::Dump {
417 previous: self.state.get(),
418 content,
419 });
420 }
421
422 pub fn show_notification(
423 &mut self,
424 content: impl Into<notification::NotificationContent>,
425 ) -> notification::Notification {
426 self.notif_hub.insert(content.into())
427 }
428
429 pub(crate) fn enter_debug(&mut self, read_ticket: ReadTicket<'_>, cursor: &Cursor) {
431 let read_ticket = if cursor.space().universe_id() == Some(self.universe.universe_id()) {
438 self.universe.read_ticket()
439 } else {
440 read_ticket
441 };
442 let content = EphemeralOpaque::new(Arc::new(crate::editor::inspect_block_at_cursor(
443 read_ticket,
444 &self.hud_inputs,
445 cursor,
446 )));
447 self.set_state(VuiPageState::Dump {
448 previous: self.state.get(),
449 content,
450 });
451 }
452
453 pub fn show_click_result(&self, button: usize, result: Result<(), ToolError>) {
456 self.cue_channel.notify(&CueMessage::Clicked(button));
457 match result {
458 Ok(()) => {}
459 Err(error) => self.show_tool_error(&error),
460 }
461 }
462
463 fn show_tool_error(&self, error: &ToolError) {
464 if let Ok(mut state) = self.tooltip_state.lock() {
466 state.set_message(error.to_string().into());
467 }
468 }
469
470 pub fn click(&mut self, _button: usize, cursor: Option<Cursor>) -> Result<(), ToolError> {
472 if cursor.as_ref().map(Cursor::space) != Option::as_ref(&self.current_view.get().space) {
473 return Err(ToolError::Internal(String::from(
474 "Vui::click: space didn't match",
475 )));
476 }
477 let transaction = Tool::Activate.use_immutable_tool(&ToolInput {
480 read_ticket: self.universe.read_ticket(),
481 cursor,
482 character: None,
483 })?;
484 transaction
485 .execute(&mut self.universe, (), &mut transaction::no_outputs)
486 .map_err(|e| ToolError::Internal(e.to_string()))?;
487 Ok(())
488 }
489
490 pub fn back(&mut self) {
494 match *self.state.get() {
495 VuiPageState::Hud => {
496 if !self.hud_inputs.paused.get() {
497 self.hud_inputs.app_control_channel.send(ControlMessage::TogglePause).unwrap();
502 }
503 }
504 VuiPageState::Paused => {
505 if self.hud_inputs.paused.get() {
506 self.hud_inputs.app_control_channel.send(ControlMessage::TogglePause).unwrap();
508 }
509 }
510 VuiPageState::AboutText | VuiPageState::Options => {
511 self.set_state(VuiPageState::Hud);
514 }
515 VuiPageState::Progress => {
516 log::error!("TODO: need UI state for dismissing notifications");
517 }
518 VuiPageState::Dump { ref previous, .. } => {
519 self.set_state(Arc::clone(previous));
520 }
521 }
522 }
523
524 pub fn quit(&self) -> impl Future<Output = QuitResult> + Send + 'static + use<> {
535 if let Some(quit_fn) = self.hud_inputs.quit.as_ref() {
536 std::future::ready(quit_fn())
537 } else {
538 std::future::ready(Err(QuitCancelled::Unsupported))
539 }
540 }
541
542 fn choose_new_page_state(&self, current_state: &VuiPageState) -> Option<VuiPageState> {
548 if self.notif_hub.has_interrupt()
550 && current_state.freely_replaceable()
551 && !matches!(current_state, VuiPageState::Progress)
552 {
553 return Some(VuiPageState::Progress);
554 }
555
556 if !self.notif_hub.has_interrupt() && matches!(current_state, VuiPageState::Progress) {
558 return Some(VuiPageState::Hud);
560 }
561
562 let paused = self.hud_inputs.paused.get();
563 if paused && matches!(current_state, VuiPageState::Hud) {
564 Some(VuiPageState::Paused)
566 } else if !paused && matches!(current_state, VuiPageState::Paused) {
567 Some(VuiPageState::Hud)
568 } else {
569 None
570 }
571 }
572}
573
574#[derive(Clone, Debug, PartialEq)]
577pub(crate) enum VuiPageState {
578 Hud,
580
581 Paused,
584
585 Options,
587
588 AboutText,
590
591 Progress,
594
595 Dump {
599 previous: Arc<VuiPageState>,
600 content: EphemeralOpaque<vui::Page>,
601 },
602}
603
604impl VuiPageState {
605 pub fn freely_replaceable(&self) -> bool {
607 match self {
608 VuiPageState::Hud => true,
609 VuiPageState::Paused => true,
610 VuiPageState::AboutText => true,
611 VuiPageState::Progress => true,
612
613 VuiPageState::Options => false,
614 VuiPageState::Dump { .. } => false,
615 }
616 }
617}
618
619#[derive(Clone, Debug)]
621pub(crate) enum VuiMessage {
622 Back,
628 Open(VuiPageState),
630}
631
632pub(crate) type CueNotifier = Arc<Notifier<CueMessage>>;
638
639#[derive(Clone, Copy, Debug)]
641pub(crate) enum CueMessage {
642 Clicked(usize),
647}
648
649impl fmt::Debug for Command {
652 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
653 f.debug_struct("Command").field("label", &self.label).finish_non_exhaustive()
654 }
655}
656
657#[cfg(test)]
660mod tests {
661 use super::*;
662
663 async fn new_vui_for_test(paused: bool) -> (Vui, flume::Receiver<ControlMessage>) {
664 let (cctx, ccrx) = flume::bounded(1);
665 let vui = Vui::new(UiTargets {
666 mouselook_mode: listen::constant(false),
667 character_source: listen::constant(None),
668 paused: listen::constant(paused),
669 graphics_options: listen::constant(Arc::new(GraphicsOptions::default())),
670 app_control_channel: cctx,
671 viewport_source: listen::constant(Viewport::ARBITRARY),
672 fullscreen_mode: listen::constant(None),
673 set_fullscreen: None,
674 quit: None,
675 custom_commands: listen::constant(Default::default()),
676 })
677 .await;
678 (vui, ccrx)
679 }
680
681 #[macro_rules_attribute::apply(smol_macros::test)]
682 async fn back_pause() {
683 let (mut vui, control_channel) = new_vui_for_test(false).await;
684 vui.back();
685 let msg = control_channel.try_recv().unwrap();
686 assert!(matches!(msg, ControlMessage::TogglePause), "{msg:?}");
687 assert!(control_channel.try_recv().is_err());
688 }
689
690 #[macro_rules_attribute::apply(smol_macros::test)]
691 async fn back_unpause() {
692 let (mut vui, control_channel) = new_vui_for_test(true).await;
693 vui.set_state(VuiPageState::Paused);
694 vui.back();
695 let msg = control_channel.try_recv().unwrap();
696 assert!(matches!(msg, ControlMessage::TogglePause), "{msg:?}");
697 assert!(control_channel.try_recv().is_err());
698 }
699}