1use std::collections::HashMap;
36use std::sync::{Arc, mpsc};
37use std::time::Instant;
38use winit::event::WindowEvent;
39
40use crate::builder::{StyleMap, build_logic};
41use crate::draw::{Pane, Scene, ShaderId};
42use crate::input::Input;
43use crate::loader::{MenuDef, UiMsg, load_menu, load_menu_soft, spawn_watcher};
44use crate::logic::Root;
45use crate::styles::StyleRegistry;
46use crate::textures::{GifMode, TextureId, TextureRegistry};
47
48#[derive(Debug, Clone, PartialEq)]
57pub enum PaneAction {
58 SwitchRoot(String),
61
62 Quit,
66
67 Custom(String),
70
71 Slider(String, f32),
73
74 TextChanged(String, String),
77
78 TextSubmitted(String, String),
80
81 Toggle(String, bool),
83
84 Dropdown(String, usize, String),
87
88 Radio(String, usize, String),
91}
92
93#[derive(Debug, Clone)]
109pub enum WriteValue {
110 Slider(f32),
112 Toggle(bool),
114 Text(String),
116 Selected(usize),
118}
119
120fn register_shaders(
128 renderer: &mut Pane,
129 device: &wgpu::Device,
130 shader_dirs: &[String],
131 tex_reg: &TextureRegistry,
132) -> (HashMap<String, ShaderId>, ShaderId) {
133 let mut map = HashMap::new();
134 for name in crate::shader_reg::BUILTIN_SHADER_NAMES {
135 let id = renderer.register_shader(device, &crate::shader_reg::load_shader(name), tex_reg);
136 map.insert(name.to_string(), id);
137 }
138 for dir in shader_dirs {
139 for (name, src) in crate::shader_reg::scan_shader_dir(dir) {
140 map.insert(name, renderer.register_shader(device, &src, tex_reg));
141 }
142 }
143 let textured_id = *map
144 .get("textured")
145 .expect("[pane_ui] built-in 'textured' shader missing");
146 (map, textured_id)
147}
148
149fn load_texture_cached(
155 tex_reg: &mut TextureRegistry,
156 device: &wgpu::Device,
157 queue: &wgpu::Queue,
158 tex_cache: &mut HashMap<String, TextureId>,
159 path: &str,
160 gif_mode: Option<GifMode>,
161) -> Option<TextureId> {
162 if let Some(&id) = tex_cache.get(path) {
163 return Some(id);
164 }
165 let result = match gif_mode {
166 Some(mode) => tex_reg.load_gif(device, queue, path, mode),
167 None => tex_reg.load(device, queue, path),
168 };
169 match result {
170 Ok(id) => {
171 tex_cache.insert(path.to_string(), id);
172 Some(id)
173 }
174 Err(e) => {
175 eprintln!("[pane_ui] texture load error \\'{path}\\': {e}");
176 None
177 }
178 }
179}
180
181pub(crate) fn init_and_build(
186 renderer: &mut Pane,
187 device: &wgpu::Device,
188 queue: &wgpu::Queue,
189 menu: &MenuDef,
190 _tx: &mpsc::Sender<UiMsg>,
191 tex_reg: &mut TextureRegistry,
192) -> (
193 StyleRegistry,
194 HashMap<String, Root>,
195 Option<String>,
196 ShaderId,
197 Option<crate::styles::StyleId>,
198 StyleMap,
199) {
200 let (shader_map, textured_id) = register_shaders(renderer, device, &menu.shader_dirs, tex_reg);
201 let mut tex_cache: HashMap<String, TextureId> = HashMap::new();
202 let mut style_registry = StyleRegistry::new();
203
204 let style_map = crate::styles_reg::register_all(
205 &mut style_registry,
206 &shader_map,
207 &menu.style_dirs,
208 textured_id,
209 &mut |path, gif_mode| {
210 load_texture_cached(tex_reg, device, queue, &mut tex_cache, path, gif_mode)
211 },
212 );
213
214 let default_style = menu.default_style.as_deref().and_then(|name| {
215 if let Some(&(id, _)) = style_map.get(name) {
216 Some(id)
217 } else {
218 eprintln!("[pane_ui] default_style '{name}' not found in style map, ignoring");
219 None
220 }
221 });
222
223 let (roots, active_root) = build_logic(menu, &style_map, textured_id, &mut |path, gif_mode| {
224 load_texture_cached(tex_reg, device, queue, &mut tex_cache, path, gif_mode)
225 });
226
227 (
228 style_registry,
229 roots,
230 active_root,
231 textured_id,
232 default_style,
233 style_map,
234 )
235}
236
237pub(crate) fn msg_to_action(msg: UiMsg) -> Option<PaneAction> {
242 match msg {
243 UiMsg::Quit => Some(PaneAction::Quit),
244 UiMsg::Custom(s) => Some(PaneAction::Custom(s)),
245 UiMsg::Slider(id, val) => Some(PaneAction::Slider(id, val)),
246 UiMsg::TextChanged(id, s) => Some(PaneAction::TextChanged(id, s)),
247 UiMsg::TextSubmitted(id, s) => Some(PaneAction::TextSubmitted(id, s)),
248 UiMsg::Toggle(tag, checked) => Some(PaneAction::Toggle(tag, checked)),
249 UiMsg::Dropdown(tag, idx, val) => Some(PaneAction::Dropdown(tag, idx, val)),
250 UiMsg::Radio(tag, idx, val) => Some(PaneAction::Radio(tag, idx, val)),
251 _ => None,
252 }
253}
254
255fn init_persistent(
267 pane: &mut Pane,
268 menu: &MenuDef,
269 tx: &mpsc::Sender<UiMsg>,
270 path_owned: &str,
271 bg_path: Option<&String>,
272 clear_color: Option<crate::draw::Color>,
273) -> crate::threader::Persistent {
274 let device = pane
275 .device()
276 .expect("standalone: device not yet initialized")
277 .clone();
278 let queue = pane
279 .queue()
280 .expect("standalone: queue not yet initialized")
281 .clone();
282 let mut tex_reg = TextureRegistry::new(&device, &queue);
283 let (new_styles, new_roots, new_active, textured_id, new_default_style, new_style_map) =
284 init_and_build(pane, &device, &queue, menu, tx, &mut tex_reg);
285 let background = bg_path.and_then(|p| {
286 let result = if p.to_lowercase().ends_with(".gif") {
287 tex_reg.load_gif(&device, &queue, p, GifMode::Loop)
288 } else {
289 tex_reg.load(&device, &queue, p)
290 };
291 result.ok().map(|id| {
292 let (width, height) = tex_reg.dimensions(id);
293 crate::threader::BackgroundDef {
294 shader: textured_id,
295 texture: id,
296 width,
297 height,
298 }
299 })
300 });
301 crate::threader::Persistent {
302 roots: new_roots,
303 active_root: new_active,
304 tab_pages: std::collections::HashMap::new(),
305 nav: crate::logic::NavState::default(),
306 styles: new_styles,
307 style_map: new_style_map,
308 default_style: new_default_style,
309 tex_reg: Some(tex_reg),
310 device: Some(device),
311 queue: Some(queue),
312 renderer: None,
313 background,
314 clear_color,
315 pending_actions: Vec::new(),
316 tx: tx.clone(),
317 rx: mpsc::channel().1,
318 ron_path: path_owned.to_string(),
319 debug: std::env::var("PANE_DEBUG").is_ok(),
320 headless_accessible: false,
321 osk: crate::keyboard::OskState::default(),
322 }
323}
324
325fn drain_messages_standalone(
334 rx: &mpsc::Receiver<UiMsg>,
335 p: &mut crate::threader::Persistent,
336 pane: &mut Pane,
337 path_owned: &str,
338 tx: &mpsc::Sender<UiMsg>,
339) {
340 while let Ok(msg) = rx.try_recv() {
341 match msg {
342 UiMsg::SwitchRoot(name) => {
343 p.active_root = Some(name.clone());
344 p.nav = crate::logic::NavState::default();
345 p.pending_actions.push(PaneAction::SwitchRoot(name));
346 }
347 UiMsg::SwitchTabPage { tab_id, page } => {
348 p.tab_pages.insert(tab_id, page);
349 }
350 UiMsg::Quit => std::process::exit(0),
351 UiMsg::Toast {
352 message,
353 duration,
354 x,
355 y,
356 width,
357 height,
358 } => {
359 push_toast_into(p, message, duration, x, y, width, height);
360 }
361 UiMsg::Reload => {
362 if let Ok(new_menu) = load_menu_soft(path_owned) {
363 let device = p.device.as_ref().unwrap().clone();
364 let queue = p.queue.as_ref().unwrap().clone();
365 let prev_active = p.active_root.clone();
366 let tex_reg = p.tex_reg.as_mut().unwrap();
367 let (
368 new_styles,
369 new_roots,
370 new_active,
371 _textured_id,
372 new_default_style,
373 new_style_map,
374 ) = init_and_build(pane, &device, &queue, &new_menu, tx, tex_reg);
375 p.styles = new_styles;
376 p.style_map = new_style_map;
377 p.active_root = prev_active
378 .filter(|name| new_roots.iter().any(|(k, _)| k == name))
379 .or(new_active);
380 p.roots = new_roots;
381 p.default_style = new_default_style;
382 p.nav = crate::logic::NavState::default();
383 p.clear_color = new_menu.clear_color;
384 }
385 }
386 other => {
387 if let Some(a) = msg_to_action(other) {
388 p.pending_actions.push(a);
389 }
390 }
391 }
392 }
393}
394
395pub fn run(path: &str) {
410 run_with(path, |_, _| {});
411}
412
413pub struct StandaloneHandle<'a> {
418 persistent: &'a mut crate::threader::Persistent,
419}
420
421impl StandaloneHandle<'_> {
422 #[must_use]
428 pub fn read(&self, id: &str) -> Option<(&crate::items::UiItem, crate::widgets::WidgetState)> {
429 read_into(self.persistent, id)
430 }
431
432 pub fn write(&mut self, id: &str, value: &WriteValue) {
436 write_into(self.persistent, id, value);
437 }
438
439 pub fn create(&mut self, root: &str, builder: impl crate::build::WidgetBuilder) -> bool {
444 create_into(self.persistent, root, builder)
445 }
446
447 pub fn destroy(&mut self, id: &str) -> bool {
449 destroy_into(self.persistent, id)
450 }
451
452 pub fn create_root(&mut self, name: impl Into<String>) {
454 create_root_into(self.persistent, name);
455 }
456
457 #[must_use]
459 pub fn style_id(&self, name: &str) -> Option<crate::styles::StyleId> {
460 style_id_in(self.persistent, name)
461 }
462
463 #[must_use]
465 pub fn default_style(&self) -> Option<&str> {
466 default_style_in(self.persistent)
467 }
468
469 pub fn set_default_style(&mut self, name: &str) -> bool {
471 set_default_style_in(self.persistent, name)
472 }
473
474 pub fn push_toast(
478 &mut self,
479 message: impl Into<String>,
480 duration: f32,
481 x: f32,
482 y: f32,
483 width: f32,
484 height: f32,
485 ) {
486 push_toast_into(
487 self.persistent,
488 message.into(),
489 duration,
490 x,
491 y,
492 width,
493 height,
494 );
495 }
496}
497
498pub fn run_with<F>(path: &str, mut on_action: F)
515where
516 F: FnMut(&mut StandaloneHandle, PaneAction) + 'static,
517{
518 let path_owned = path.to_string();
519 let menu = load_menu(&path_owned);
520
521 let (tx, rx) = mpsc::channel::<UiMsg>();
522 if menu.hot_reload {
523 spawn_watcher(
524 path_owned.clone(),
525 menu.shader_dirs.clone(),
526 menu.style_dirs.clone(),
527 tx.clone(),
528 );
529 }
530
531 let bg_path = menu.background.clone();
532 let clear_color = menu.clear_color;
533 let mut persistent: Option<crate::threader::Persistent> = None;
534 let mut last_time = Instant::now();
535
536 crate::draw::run(
537 move |pane: &mut Pane, scene: &mut Scene, input: &mut Input, pw: f32, ph: f32, _dt: f32| {
538 if persistent.is_none() {
540 persistent = Some(init_persistent(
541 pane,
542 &menu,
543 &tx,
544 &path_owned,
545 bg_path.as_ref(),
546 clear_color,
547 ));
548 }
549
550 let p = persistent.as_mut().unwrap();
551
552 drain_messages_standalone(&rx, p, pane, &path_owned, &tx);
554
555 let mut frame = crate::threader::Frame {
556 dt: 0.0,
557 last_tick: last_time,
558 pw,
559 ph,
560 input: std::mem::take(input),
561 scene: std::mem::take(scene),
562 };
563 {
564 let mut ctx = crate::threader::FrameCtx {
565 persistent: p,
566 frame: &mut frame,
567 standalone_pane: Some(pane),
568 };
569 crate::order::run_frame(&mut ctx, None, None);
570 }
571 last_time = frame.last_tick;
572 *input = frame.input;
573 *scene = frame.scene;
574
575 let actions = std::mem::take(&mut p.pending_actions);
576 let cc = p.clear_color;
577 let cursor = [
578 input.mouse_x,
579 input.mouse_y,
580 f32::from(u8::from(input.left_pressed)),
581 ];
582 let mut handle = StandaloneHandle { persistent: p };
583 for action in actions {
584 on_action(&mut handle, action);
585 }
586
587 pane.present_standalone(
588 scene,
589 pw,
590 ph,
591 frame.dt,
592 cursor,
593 cc,
594 p.tex_reg.as_mut().unwrap(),
595 );
596 scene.clear();
597 },
598 );
599}
600
601pub struct PaneOverlay {
611 persistent: crate::threader::Persistent,
612 input: Input,
613 scene: Scene,
614 last_time: Instant,
615 gilrs: Option<gilrs::Gilrs>,
616}
617
618#[must_use]
631pub fn overlay(
632 path: &str,
633 device: &wgpu::Device,
634 queue: &wgpu::Queue,
635 format: wgpu::TextureFormat,
636 gilrs: Option<gilrs::Gilrs>,
637) -> PaneOverlay {
638 let menu = load_menu(path);
639 let (tx, rx) = mpsc::channel::<UiMsg>();
640 let mut renderer = Pane::new(device, queue, format);
641 let mut tex_reg = TextureRegistry::new(device, queue);
642 let (new_styles, new_roots, new_active, _textured_id, new_default_style, new_style_map) =
643 init_and_build(&mut renderer, device, queue, &menu, &tx, &mut tex_reg);
644
645 if menu.hot_reload {
646 spawn_watcher(
647 path.to_string(),
648 menu.shader_dirs.clone(),
649 menu.style_dirs.clone(),
650 tx.clone(),
651 );
652 }
653
654 let device_arc = Arc::new(device.clone());
656 let queue_arc = Arc::new(queue.clone());
657
658 let persistent = crate::threader::Persistent {
659 roots: new_roots,
660 active_root: new_active,
661 tab_pages: std::collections::HashMap::new(),
662 nav: crate::logic::NavState::default(),
663 styles: new_styles,
664 style_map: new_style_map,
665 default_style: new_default_style,
666 tex_reg: Some(tex_reg),
667 device: Some(device_arc),
668 queue: Some(queue_arc),
669 renderer: Some(renderer),
670 background: None,
671 clear_color: None,
672 pending_actions: Vec::new(),
673 tx,
674 rx,
675 ron_path: path.to_string(),
676 debug: false,
677 headless_accessible: false,
678 osk: crate::keyboard::OskState::default(),
679 };
680
681 PaneOverlay {
682 persistent,
683 input: Input::new(),
684 scene: Scene::new(),
685 last_time: Instant::now(),
686 gilrs: gilrs.or_else(|| gilrs::Gilrs::new().ok()),
687 }
688}
689
690fn active_root_mut(
694 persistent: &mut crate::threader::Persistent,
695) -> Option<&mut crate::logic::Root> {
696 persistent
697 .active_root
698 .as_deref()
699 .and_then(|n| persistent.roots.get_mut(n))
700}
701
702fn style_id_in(
707 persistent: &crate::threader::Persistent,
708 name: &str,
709) -> Option<crate::styles::StyleId> {
710 persistent.style_map.get(name).map(|&(id, _)| id)
711}
712
713fn default_style_in(persistent: &crate::threader::Persistent) -> Option<&str> {
714 let id = persistent.default_style?;
715 persistent
716 .style_map
717 .iter()
718 .find(|(_, v)| v.0 == id)
719 .map(|(name, _)| name.as_str())
720}
721
722fn set_default_style_in(persistent: &mut crate::threader::Persistent, name: &str) -> bool {
723 match persistent.style_map.get(name).map(|&(id, _)| id) {
724 Some(id) => {
725 persistent.default_style = Some(id);
726 true
727 }
728 None => false,
729 }
730}
731
732fn create_into(
734 persistent: &mut crate::threader::Persistent,
735 root: &str,
736 builder: impl crate::build::WidgetBuilder,
737) -> bool {
738 let (item, state) = builder.build(persistent.default_style);
739 let id = item.id().to_string();
740 for r in persistent.roots.values() {
741 if r.items.iter().any(|(i, _)| i.id() == id) {
742 eprintln!("[pane_ui] create: id '{id}' already exists");
743 return false;
744 }
745 }
746 let Some(r) = persistent.roots.get_mut(root) else {
747 eprintln!("[pane_ui] create: root '{root}' not found");
748 return false;
749 };
750 r.items.push((item, state));
751 true
752}
753
754fn destroy_into(persistent: &mut crate::threader::Persistent, id: &str) -> bool {
756 for r in persistent.roots.values_mut() {
757 if let Some(idx) = r.items.iter().position(|(item, _)| item.id() == id) {
758 r.items.remove(idx);
759 if persistent.nav.focused_id.as_deref() == Some(id) {
760 persistent.nav = crate::logic::NavState::default();
761 }
762 return true;
763 }
764 }
765 false
766}
767
768fn create_root_into(persistent: &mut crate::threader::Persistent, name: impl Into<String>) {
770 let name = name.into();
771 persistent
772 .roots
773 .entry(name)
774 .or_insert_with(crate::logic::Root::new);
775}
776
777fn read_into<'a>(
779 persistent: &'a crate::threader::Persistent,
780 id: &str,
781) -> Option<(&'a crate::items::UiItem, crate::widgets::WidgetState)> {
782 for root in persistent.roots.values() {
783 for (item, state) in &root.items {
784 if item.id() == id {
785 return Some((item, state.visual()));
786 }
787 }
788 }
789 None
790}
791
792fn write_into(persistent: &mut crate::threader::Persistent, id: &str, value: &WriteValue) {
793 use crate::items::UiItem;
794 use crate::widgets::ItemState;
795 for root in persistent.roots.values_mut() {
796 for (item, state) in &mut root.items {
797 if item.id() != id {
798 continue;
799 }
800 match (item, state, value) {
801 (UiItem::Slider(s), ItemState::Slider(st), WriteValue::Slider(v)) => {
802 st.value = v.clamp(s.min, s.max);
803 }
804 (UiItem::Toggle(_), ItemState::Toggle(st), WriteValue::Toggle(v)) => {
805 st.checked = *v;
806 }
807 (UiItem::TextBox(tb), ItemState::TextBox(st), WriteValue::Text(v)) => {
808 st.text = tb
809 .max_len
810 .map_or_else(|| v.clone(), |limit| v.chars().take(limit).collect());
811 st.cursor_pos = st.text.chars().count();
812 }
813 (UiItem::Dropdown(dd), ItemState::Dropdown(st), WriteValue::Selected(i)) => {
814 st.selected = (*i).min(dd.items.len().saturating_sub(1));
815 }
816 (UiItem::RadioGroup(rg), ItemState::RadioGroup(st), WriteValue::Selected(i)) => {
817 st.selected = (*i).min(rg.items.len().saturating_sub(1));
818 }
819 _ => {
820 eprintln!("[pane_ui] write('{id}'): value type does not match widget type");
821 }
822 }
823 return;
824 }
825 }
826 eprintln!("[pane_ui] write('{id}'): no widget found");
827}
828
829pub(crate) fn push_toast_into(
834 persistent: &mut crate::threader::Persistent,
835 message: String,
836 duration: f32,
837 x: f32,
838 y: f32,
839 width: f32,
840 height: f32,
841) {
842 if let Some(name) = persistent.active_root.as_deref()
843 && let Some(root) = persistent.roots.get_mut(name)
844 {
845 let id = format!("__toast_{}", root.items.len());
847 root.items.push((
848 crate::items::UiItem::Toast(crate::items::Toast {
849 id,
850 x,
851 y,
852 width,
853 height,
854 message,
855 duration,
856 shape: persistent.default_style,
857 }),
858 crate::widgets::ItemState::Toast(crate::widgets::ToastState::new(duration)),
859 ));
860 }
861}
862
863fn actor_move_to_into(
865 persistent: &mut crate::threader::Persistent,
866 id: &str,
867 x: f32,
868 y: f32,
869 speed: f32,
870) {
871 if let Some(r) = active_root_mut(persistent) {
872 crate::query::actor_move_to_in(&mut r.items, id, x, y, speed);
873 }
874}
875
876fn actor_follow_cursor_into(
878 persistent: &mut crate::threader::Persistent,
879 id: &str,
880 speed: f32,
881 trail: f32,
882) {
883 if let Some(r) = active_root_mut(persistent) {
884 crate::query::actor_follow_cursor_in(&mut r.items, id, speed, trail);
885 }
886}
887
888fn actor_reset_into(persistent: &mut crate::threader::Persistent, id: &str) {
890 if let Some(r) = active_root_mut(persistent) {
891 crate::query::actor_reset_in(&mut r.items, id);
892 }
893}
894
895fn actor_set_pos_into(persistent: &mut crate::threader::Persistent, id: &str, x: f32, y: f32) {
897 if let Some(r) = active_root_mut(persistent) {
898 crate::query::actor_set_pos_in(&mut r.items, id, x, y);
899 }
900}
901
902impl PaneOverlay {
903 pub fn handle_event(&mut self, event: &WindowEvent, pw: f32, ph: f32) {
905 self.input.handle_event(event, pw, ph);
906 }
907
908 pub fn handle_gamepad_event(&mut self, event: gilrs::Event) {
913 self.input.handle_gamepad_event(event);
914 }
915
916 pub fn disable_auto_gamepad(&mut self) {
918 self.gilrs = None;
919 }
920
921 pub fn draw(
930 &mut self,
931 encoder: &mut wgpu::CommandEncoder,
932 view: &wgpu::TextureView,
933 pw: f32,
934 ph: f32,
935 ) -> Vec<PaneAction> {
936 if let Some(gilrs) = self.gilrs.as_mut() {
938 while let Some(ev) = gilrs.next_event() {
939 self.input.handle_gamepad_event(ev);
940 }
941 }
942
943 self.persistent.pending_actions.clear();
944
945 let mut frame = crate::threader::Frame {
946 dt: 0.0,
947 last_tick: self.last_time,
948 pw,
949 ph,
950 input: std::mem::take(&mut self.input),
951 scene: std::mem::take(&mut self.scene),
952 };
953 {
954 let mut ctx = crate::threader::FrameCtx {
955 persistent: &mut self.persistent,
956 frame: &mut frame,
957 standalone_pane: None,
958 };
959 crate::order::run_frame(&mut ctx, Some(encoder), Some(view));
960 }
961 self.last_time = frame.last_tick;
962 self.input = frame.input;
963 self.scene = frame.scene;
964
965 std::mem::take(&mut self.persistent.pending_actions)
966 }
967
968 pub fn read(&self, id: &str) -> Option<(&crate::items::UiItem, crate::widgets::WidgetState)> {
982 read_into(&self.persistent, id)
983 }
984
985 pub fn write(&mut self, id: &str, value: &WriteValue) {
992 write_into(&mut self.persistent, id, value);
993 }
994
995 pub fn create(&mut self, root: &str, builder: impl crate::build::WidgetBuilder) -> bool {
1000 create_into(&mut self.persistent, root, builder)
1001 }
1002
1003 pub fn destroy(&mut self, id: &str) -> bool {
1005 destroy_into(&mut self.persistent, id)
1006 }
1007
1008 pub fn create_root(&mut self, name: impl Into<String>) {
1010 create_root_into(&mut self.persistent, name);
1011 }
1012
1013 #[must_use]
1015 pub fn style_id(&self, name: &str) -> Option<crate::styles::StyleId> {
1016 style_id_in(&self.persistent, name)
1017 }
1018
1019 #[must_use]
1021 pub fn default_style(&self) -> Option<&str> {
1022 default_style_in(&self.persistent)
1023 }
1024
1025 pub fn set_default_style(&mut self, name: &str) -> bool {
1027 set_default_style_in(&mut self.persistent, name)
1028 }
1029
1030 pub const fn set_debug(&mut self, on: bool) {
1032 self.persistent.debug = on;
1033 }
1034
1035 pub fn push_toast(
1039 &mut self,
1040 message: impl Into<String>,
1041 duration: f32,
1042 x: f32,
1043 y: f32,
1044 width: f32,
1045 height: f32,
1046 ) {
1047 push_toast_into(
1048 &mut self.persistent,
1049 message.into(),
1050 duration,
1051 x,
1052 y,
1053 width,
1054 height,
1055 );
1056 }
1057
1058 pub fn actor_move_to(&mut self, id: &str, x: f32, y: f32, speed: f32) {
1062 actor_move_to_into(&mut self.persistent, id, x, y, speed);
1063 }
1064
1065 pub fn actor_follow_cursor(&mut self, id: &str, speed: f32, trail: f32) {
1067 actor_follow_cursor_into(&mut self.persistent, id, speed, trail);
1068 }
1069
1070 pub fn actor_reset(&mut self, id: &str) {
1072 actor_reset_into(&mut self.persistent, id);
1073 }
1074
1075 pub fn actor_set_pos(&mut self, id: &str, x: f32, y: f32) {
1077 actor_set_pos_into(&mut self.persistent, id, x, y);
1078 }
1079}
1080
1081pub struct PaneHeadless {
1091 persistent: crate::threader::Persistent,
1092 input: Input,
1093 scene: Scene,
1094}
1095
1096#[must_use]
1107pub fn headless(path: &str) -> PaneHeadless {
1108 let menu = load_menu(path);
1109 let (tx, rx) = mpsc::channel::<UiMsg>();
1110 let dummy_shader = ShaderId::new(0);
1111
1112 let style_map: StyleMap = crate::styles_reg::BUILTIN_STYLE_NAMES
1114 .iter()
1115 .enumerate()
1116 .map(|(i, name)| {
1117 (
1118 name.to_string(),
1119 (crate::styles::StyleId::new(i), dummy_shader),
1120 )
1121 })
1122 .collect();
1123
1124 let (roots, active_root) = build_logic(&menu, &style_map, dummy_shader, &mut |_, _| None);
1125
1126 let default_style = menu
1127 .default_style
1128 .as_deref()
1129 .and_then(|name| style_map.get(name).map(|&(id, _)| id));
1130
1131 let persistent = crate::threader::Persistent {
1132 roots,
1133 active_root,
1134 tab_pages: std::collections::HashMap::new(),
1135 nav: crate::logic::NavState::default(),
1136 styles: StyleRegistry::new(),
1137 style_map,
1138 default_style,
1139 tex_reg: None,
1140 device: None,
1141 queue: None,
1142 renderer: None,
1143 background: None,
1144 clear_color: None,
1145 pending_actions: Vec::new(),
1146 tx,
1147 rx,
1148 ron_path: path.to_string(),
1149 debug: false,
1150 headless_accessible: menu.headless_accessible,
1151 osk: crate::keyboard::OskState::default(),
1152 };
1153
1154 PaneHeadless {
1155 persistent,
1156 input: Input::new(),
1157 scene: Scene::new(),
1158 }
1159}
1160
1161impl PaneHeadless {
1162 pub fn update(&mut self, dt: f32) -> Vec<PaneAction> {
1167 self.persistent.pending_actions.clear();
1168
1169 let last_tick = std::time::Instant::now()
1171 .checked_sub(std::time::Duration::from_secs_f32(dt))
1172 .unwrap_or_else(std::time::Instant::now);
1173
1174 let mut frame = crate::threader::Frame {
1175 dt: 0.0,
1176 last_tick,
1177 pw: 0.0,
1178 ph: 0.0,
1179 input: std::mem::take(&mut self.input),
1180 scene: std::mem::take(&mut self.scene),
1181 };
1182 {
1183 let mut ctx = crate::threader::FrameCtx {
1184 persistent: &mut self.persistent,
1185 frame: &mut frame,
1186 standalone_pane: None,
1187 };
1188 crate::order::run_frame(&mut ctx, None, None);
1189 }
1190 self.input = frame.input;
1191 self.scene = frame.scene;
1192
1193 std::mem::take(&mut self.persistent.pending_actions)
1194 }
1195
1196 pub fn press(&mut self, id: &str) {
1201 if !self.persistent.headless_accessible {
1202 eprintln!("[pane_ui] press('{id}') ignored — headless_accessible is false");
1203 return;
1204 }
1205 let tx = self.persistent.tx.clone();
1206 let debug = self.persistent.debug;
1207 let Some(name) = self.persistent.active_root.clone() else {
1208 eprintln!("[pane_ui] press('{id}'): no active root");
1209 return;
1210 };
1211 if let Some(root) = self.persistent.roots.get_mut(&name) {
1212 let found = root.items.iter_mut().any(|(item, state)| {
1213 use crate::items::UiItem;
1214 use crate::widgets::ItemState;
1215 if let (UiItem::Toggle(t), ItemState::Toggle(s)) = (&*item, state)
1216 && t.id == id
1217 {
1218 {
1219 s.checked = !s.checked;
1220 match &t.action {
1221 crate::loader::ToggleAction::Custom(tag) => {
1222 let _ =
1223 tx.send(crate::loader::UiMsg::Toggle(tag.clone(), s.checked));
1224 }
1225 crate::loader::ToggleAction::Print => {
1226 println!("{}: {}", t.id, s.checked);
1227 }
1228 }
1229 return true;
1230 }
1231 }
1232 crate::logic::press_in_item(&tx, item, id, debug)
1233 });
1234 if !found {
1235 eprintln!("[pane_ui] press('{id}'): no button found");
1236 }
1237 }
1238 }
1239
1240 pub fn active_root(&self) -> Option<&str> {
1242 self.persistent.active_root.as_deref()
1243 }
1244
1245 pub fn read(&self, id: &str) -> Option<(&crate::items::UiItem, crate::widgets::WidgetState)> {
1251 read_into(&self.persistent, id)
1252 }
1253
1254 pub fn write(&mut self, id: &str, value: &WriteValue) {
1261 write_into(&mut self.persistent, id, value);
1262 }
1263
1264 pub fn create(&mut self, root: &str, builder: impl crate::build::WidgetBuilder) -> bool {
1269 create_into(&mut self.persistent, root, builder)
1270 }
1271
1272 pub fn destroy(&mut self, id: &str) -> bool {
1274 destroy_into(&mut self.persistent, id)
1275 }
1276
1277 pub fn create_root(&mut self, name: impl Into<String>) {
1279 create_root_into(&mut self.persistent, name);
1280 }
1281
1282 #[must_use]
1284 pub fn style_id(&self, name: &str) -> Option<crate::styles::StyleId> {
1285 style_id_in(&self.persistent, name)
1286 }
1287
1288 #[must_use]
1290 pub fn default_style(&self) -> Option<&str> {
1291 default_style_in(&self.persistent)
1292 }
1293
1294 pub fn set_default_style(&mut self, name: &str) -> bool {
1296 set_default_style_in(&mut self.persistent, name)
1297 }
1298
1299 pub const fn set_debug(&mut self, on: bool) {
1302 self.persistent.debug = on;
1303 }
1304
1305 pub fn push_toast(
1311 &mut self,
1312 message: impl Into<String>,
1313 duration: f32,
1314 x: f32,
1315 y: f32,
1316 width: f32,
1317 height: f32,
1318 ) {
1319 push_toast_into(
1320 &mut self.persistent,
1321 message.into(),
1322 duration,
1323 x,
1324 y,
1325 width,
1326 height,
1327 );
1328 }
1329
1330 pub fn actor_move_to(&mut self, id: &str, x: f32, y: f32, speed: f32) {
1334 actor_move_to_into(&mut self.persistent, id, x, y, speed);
1335 }
1336
1337 pub fn actor_follow_cursor(&mut self, id: &str, speed: f32, trail: f32) {
1339 actor_follow_cursor_into(&mut self.persistent, id, speed, trail);
1340 }
1341
1342 pub fn actor_reset(&mut self, id: &str) {
1344 actor_reset_into(&mut self.persistent, id);
1345 }
1346
1347 pub fn actor_set_pos(&mut self, id: &str, x: f32, y: f32) {
1349 actor_set_pos_into(&mut self.persistent, id, x, y);
1350 }
1351}