1use serde::Deserialize;
2use std::sync::mpsc;
3use std::time::{Duration, SystemTime};
4
5#[derive(Debug)]
8pub(crate) enum UiMsg {
9 SwitchRoot(String),
10 SwitchTabPage {
11 tab_id: String,
12 page: usize,
13 },
14 Quit,
15 Custom(String),
16 Reload,
17 Slider(String, f32),
18 TextChanged(String, String),
19 TextSubmitted(String, String),
20 Toggle(String, bool),
21 Dropdown(String, usize, String),
22 Radio(String, usize, String),
23 Toast {
25 message: String,
26 duration: f32,
27 x: f32,
28 y: f32,
29 width: f32,
30 height: f32,
31 },
32}
33
34#[derive(Deserialize)]
37pub struct MenuDef {
38 #[serde(default)]
39 pub hot_reload: bool,
40 #[serde(default)]
41 pub headless_accessible: bool,
42 #[serde(default)]
43 pub shader_dirs: Vec<String>,
44 #[serde(default)]
45 pub style_dirs: Vec<String>,
46 pub background: Option<String>,
47 #[serde(default)]
48 pub clear_color: Option<crate::draw::Color>,
49 pub default_style: Option<String>,
50 pub start_root: String,
51 pub roots: Vec<RootDef>,
52}
53
54#[derive(Deserialize)]
55pub struct RootDef {
56 pub name: String,
57 #[serde(default)]
58 pub buttons: Vec<ButtonDef>,
59 #[serde(default)]
60 pub scroll_lists: Vec<ScrollListDef>,
61 #[serde(default)]
62 pub bars: Vec<BarDef>,
63 #[serde(default)]
64 pub popouts: Vec<PopoutDef>,
65 #[serde(default)]
66 pub toggles: Vec<ToggleDef>,
67 #[serde(default)]
68 pub sliders: Vec<SliderDef>,
69 #[serde(default)]
70 pub labels: Vec<FreeLabelDef>,
71 #[serde(default)]
72 pub dividers: Vec<DividerDef>,
73 #[serde(default)]
74 pub images: Vec<ImageDef>,
75 #[serde(default)]
76 pub text_boxes: Vec<TextBoxDef>,
77 #[serde(default)]
78 pub progress_bars: Vec<ProgressBarDef>,
79 #[serde(default)]
80 pub scroll_panes: Vec<ScrollPaneDef>,
81 #[serde(default)]
82 pub dropdowns: Vec<DropdownDef>,
83 #[serde(default)]
84 pub radio_groups: Vec<RadioGroupDef>,
85 #[serde(default)]
86 pub actors: Vec<ActorDef>,
87 #[serde(default)]
88 pub tabs: Vec<TabDef>,
89}
90
91#[derive(Deserialize)]
92pub struct BarDef {
93 pub id: String,
94 #[serde(default)]
95 pub edge: Option<BarEdgeDef>,
96 pub thickness: f32,
97 #[serde(default)]
98 pub pad: f32,
99 #[serde(default)]
100 pub gap: f32,
101 pub style: Option<String>,
102 pub items: Vec<BarItemDef>,
103 #[serde(default)]
104 pub manual: bool,
105 #[serde(default)]
107 pub x: f32,
108 #[serde(default)]
110 pub y: f32,
111 #[serde(default)]
113 pub width: f32,
114 #[serde(default)]
116 pub height: f32,
117}
118
119#[derive(Deserialize, PartialEq, Eq)]
120pub enum BarEdgeDef {
121 Top,
122 Bottom,
123 Left,
124 Right,
125 Free,
126}
127
128#[derive(Deserialize)]
129pub struct PopoutDef {
130 pub id: String,
131 #[serde(default)]
132 pub closed_x: f32,
133 #[serde(default)]
134 pub closed_y: f32,
135 #[serde(default)]
136 pub open_x: f32,
137 #[serde(default)]
138 pub open_y: f32,
139 pub width: f32,
140 pub height: f32,
141 pub toggle_id: String,
142 pub style: Option<String>,
143 pub edge: Option<PopoutEdgeDef>,
144 #[serde(default = "default_true")]
145 pub shadow: bool,
146 #[serde(default)]
147 pub horizontal: bool,
148 #[serde(default)]
149 pub gap: f32,
150 #[serde(default)]
151 pub full_span: bool,
152 #[serde(default)]
153 pub home_toggles: bool,
154 #[serde(default)]
155 pub items: Vec<PopoutItemDef>,
156}
157
158const fn default_true() -> bool {
159 true
160}
161
162#[derive(Deserialize, Clone, Copy)]
163pub enum PopoutEdgeDef {
164 Left,
165 Right,
166 Top,
167 Bottom,
168}
169
170#[derive(Deserialize)]
171pub enum PopoutItemDef {
172 Button(ButtonDef),
173 ScrollList(ScrollListDef),
174 Bar(BarDef),
175 Popout(PopoutDef),
176}
177
178#[derive(Deserialize)]
179pub enum BarItemDef {
180 Button(ListButtonDef),
181 Label(LabelDef),
182 Spacer,
183 ScrollList(ScrollListDef),
184}
185
186#[derive(Deserialize)]
187pub struct LabelDef {
188 pub id: String,
189 pub text: String,
190 #[serde(default = "default_label_size")]
191 pub size: f32,
192 pub color: Option<crate::draw::Color>,
193 #[serde(default)]
194 pub width: f32,
195}
196
197const fn default_label_size() -> f32 {
198 24.0
199}
200
201#[derive(Deserialize)]
202pub struct ButtonDef {
203 pub id: String,
204 pub x: f32,
205 pub y: f32,
206 pub width: f32,
207 pub height: f32,
208 pub text: String,
209 pub tooltip: Option<String>,
210 pub style: Option<String>,
211 pub on_press: PressAction,
212 #[serde(default)]
213 pub nav_default: bool,
214}
215
216#[derive(Deserialize)]
217pub struct ScrollListDef {
218 pub id: String,
219 pub x: f32,
220 pub y: f32,
221 pub width: f32,
222 pub height: f32,
223 #[serde(default)]
224 pub pad_left: f32,
225 #[serde(default)]
226 pub pad_right: f32,
227 #[serde(default)]
228 pub pad_top: f32,
229 #[serde(default)]
230 pub pad_bottom: f32,
231 #[serde(default)]
232 pub gap: f32,
233 pub style: Option<String>,
234 #[serde(default)]
235 pub horizontal: bool,
236 #[serde(default)]
237 pub full_span: bool,
238 pub items: Vec<ListButtonDef>,
239}
240
241#[derive(Deserialize)]
242pub struct ListButtonDef {
243 pub id: String,
244 pub height: f32,
245 #[serde(default)]
247 pub width: f32,
248 pub text: String,
249 pub tooltip: Option<String>,
250 pub style: Option<String>,
251 pub on_press: PressAction,
252}
253
254#[derive(Deserialize, Clone)]
255pub enum PressAction {
256 SwitchRoot(String),
258 SwitchTabPage { tab_id: String, page: usize },
260 Quit,
262 Print(String),
264 Custom(String),
266 Toast {
271 message: String,
273 #[serde(default = "default_toast_duration")]
275 duration: f32,
276 #[serde(default)]
278 x: f32,
279 #[serde(default)]
281 y: f32,
282 #[serde(default = "default_toast_width")]
284 width: f32,
285 #[serde(default = "default_toast_height")]
287 height: f32,
288 },
289 RadioSelect { group_id: String, index: usize },
291 DropdownSelect { dropdown_id: String, index: usize },
293}
294
295const fn default_toast_duration() -> f32 {
296 2.0
297}
298const fn default_toast_width() -> f32 {
299 400.0
300}
301const fn default_toast_height() -> f32 {
302 60.0
303}
304
305impl PressAction {
306 #[must_use]
308 pub const fn is_internal(&self) -> bool {
309 matches!(self, Self::RadioSelect { .. } | Self::DropdownSelect { .. })
310 }
311}
312
313#[derive(Deserialize)]
314pub struct ToggleDef {
315 pub id: String,
316 pub x: f32,
317 pub y: f32,
318 pub width: f32,
319 pub height: f32,
320 pub text: String,
321 pub tooltip: Option<String>,
322 #[serde(default)]
323 pub checked: bool,
324 pub style_off: String,
325 pub style_on: String,
326 pub on_change: ToggleAction,
327}
328
329#[derive(Deserialize, Clone)]
330pub enum ToggleAction {
331 Print,
332 Custom(String),
333}
334
335pub(crate) fn load_menu_soft(path: &str) -> Result<MenuDef, String> {
340 let src = std::fs::read_to_string(path).map_err(|e| e.to_string())?;
341 ron::Options::default()
342 .with_default_extension(
343 ron::extensions::Extensions::IMPLICIT_SOME
344 | ron::extensions::Extensions::UNWRAP_NEWTYPES,
345 )
346 .from_str(&src)
347 .map_err(|e| {
348 eprintln!("[pane_ui] Reload parse error: {e}");
349 e.to_string()
350 })
351}
352
353pub(crate) fn load_menu(path: &str) -> MenuDef {
356 load_menu_soft(path).unwrap_or_else(|e| panic!("[pane_ui] Failed to load '{path}': {e}"))
357}
358
359pub(crate) fn spawn_watcher(
364 path: String,
365 shader_dirs: Vec<String>,
366 style_dirs: Vec<String>,
367 tx: mpsc::Sender<UiMsg>,
368) {
369 std::thread::spawn(move || {
370 let collect_stamps = || -> Vec<(String, Option<SystemTime>)> {
371 let mut stamps = Vec::new();
372 stamps.push((
373 path.clone(),
374 std::fs::metadata(&path).and_then(|m| m.modified()).ok(),
375 ));
376 for dir in &shader_dirs {
377 if let Ok(entries) = std::fs::read_dir(dir) {
378 for entry in entries.filter_map(std::result::Result::ok) {
379 let p = entry.path();
380 if p.extension().is_some_and(|x| x == "wgsl") {
381 stamps.push((
382 p.to_string_lossy().to_string(),
383 std::fs::metadata(&p).and_then(|m| m.modified()).ok(),
384 ));
385 }
386 }
387 }
388 }
389 for dir in &style_dirs {
390 if let Ok(entries) = std::fs::read_dir(dir) {
391 for entry in entries.filter_map(std::result::Result::ok) {
392 let p = entry.path();
393 if p.extension().is_some_and(|x| x == "ron") {
394 stamps.push((
395 p.to_string_lossy().to_string(),
396 std::fs::metadata(&p).and_then(|m| m.modified()).ok(),
397 ));
398 }
399 }
400 }
401 }
402 stamps
403 };
404
405 let mut last_stamps = collect_stamps();
406 loop {
407 std::thread::sleep(Duration::from_secs(1));
408 let new_stamps = collect_stamps();
409 if new_stamps != last_stamps {
410 let _ = tx.send(UiMsg::Reload);
411 }
412 last_stamps = new_stamps;
413 }
414 });
415}
416
417#[derive(Deserialize)]
420pub struct SliderDef {
421 pub id: String,
422 pub x: f32,
423 pub y: f32,
424 pub width: f32,
425 pub height: f32,
426 pub min: f32,
427 pub max: f32,
428 pub value: f32,
429 #[serde(default)]
430 pub step: Option<f32>,
431 pub tooltip: Option<String>,
432 pub style_track: String,
433 pub style_thumb: String,
434 pub on_change: SliderAction,
435}
436
437#[derive(Deserialize)]
440pub struct FreeLabelDef {
441 pub id: String,
442 pub x: f32,
443 pub y: f32,
444 pub text: String,
445 #[serde(default = "default_label_size")]
446 pub size: f32,
447 pub color: Option<crate::draw::Color>,
448 #[serde(default)]
449 pub width: f32,
450}
451
452#[derive(Deserialize)]
453pub struct DividerDef {
454 pub id: String,
455 pub x: f32,
456 pub y: f32,
457 pub width: f32,
458 pub height: f32,
459 pub style: String,
460 #[serde(default)]
461 pub full_span: bool,
462}
463
464#[derive(Deserialize)]
465pub struct ImageDef {
466 pub id: String,
467 pub x: f32,
468 pub y: f32,
469 pub width: f32,
470 pub height: f32,
471 pub path: String,
472 #[serde(default)]
473 pub gif_mode: Option<crate::textures::GifMode>,
474}
475
476#[derive(Deserialize)]
477pub struct TextBoxDef {
478 pub id: String,
479 pub x: f32,
480 pub y: f32,
481 pub width: f32,
482 pub height: f32,
483 #[serde(default)]
484 pub hint: String,
485 #[serde(default)]
486 pub max_len: Option<usize>,
487 pub tooltip: Option<String>,
488 pub style: String,
489 #[serde(default)]
490 pub style_focus: Option<String>,
491 pub on_change: TextBoxAction,
492 pub on_submit: TextBoxAction,
493 #[serde(default)]
494 pub password: bool,
495 #[serde(default)]
496 pub multiline: bool,
497 #[serde(default)]
498 pub rows: Option<u32>,
499 #[serde(default)]
500 pub font_size: Option<f32>,
501}
502
503#[derive(Deserialize)]
504pub struct ProgressBarDef {
505 pub id: String,
506 pub x: f32,
507 pub y: f32,
508 pub width: f32,
509 pub height: f32,
510 pub value: f32,
511 pub style_track: String,
512 pub style_fill: String,
513}
514
515#[derive(Deserialize, Clone)]
516pub enum TextBoxAction {
517 Print,
518 Custom(String),
519 None,
520}
521
522#[derive(Deserialize, Clone)]
523pub enum SliderAction {
524 Print,
525 Custom(String),
526}
527
528#[derive(Deserialize)]
531pub struct ScrollPaneDef {
532 pub id: String,
533 pub x: f32,
534 pub y: f32,
535 pub width: f32,
536 pub height: f32,
537 #[serde(default)]
538 pub pad_left: f32,
539 #[serde(default)]
540 pub pad_right: f32,
541 #[serde(default)]
542 pub pad_top: f32,
543 #[serde(default)]
544 pub pad_bottom: f32,
545 #[serde(default)]
546 pub gap: f32,
547 pub style: Option<String>,
548 #[serde(default)]
549 pub horizontal: bool,
550 #[serde(default)]
551 pub full_span: bool,
552 #[serde(default)]
553 pub manual: bool,
554 pub items: Vec<ContainerItemDef>,
555}
556
557#[derive(Deserialize)]
558pub enum ContainerItemDef {
559 Button(ButtonDef),
560 Toggle(ToggleDef),
561 Slider(SliderDef),
562 TextBox(TextBoxDef),
563 Label(FreeLabelDef),
564 Divider(DividerDef),
565 Image(ImageDef),
566 ProgressBar(ProgressBarDef),
567 ScrollPane(ScrollPaneDef),
568 ScrollList(ScrollListDef),
569 Tab(TabDef),
570}
571
572#[derive(Deserialize)]
575pub struct TabPageDef {
576 pub label: String,
577 pub items: Vec<ContainerItemDef>,
578}
579
580#[derive(Deserialize)]
581pub struct TabDef {
582 pub id: String,
583 pub x: f32,
584 pub y: f32,
585 pub width: f32,
586 pub height: f32,
587 #[serde(default)]
588 pub pad_left: f32,
589 #[serde(default)]
590 pub pad_right: f32,
591 #[serde(default)]
592 pub pad_top: f32,
593 #[serde(default)]
594 pub pad_bottom: f32,
595 #[serde(default)]
596 pub gap: f32,
597 pub style: Option<String>,
598 pub pages: Vec<TabPageDef>,
599 #[serde(default)]
600 pub full_span: bool,
601}
602
603#[derive(Deserialize)]
606pub struct DropdownDef {
607 pub id: String,
608 pub x: f32,
609 pub y: f32,
610 pub width: f32,
611 pub height: f32,
612 pub options: Vec<String>,
613 #[serde(default)]
614 pub selected: usize,
615 pub tooltip: Option<String>,
616 pub style: Option<String>,
617 pub style_list: Option<String>,
618 pub style_item: Option<String>,
619 pub on_change: DropdownAction,
620}
621
622#[derive(Deserialize, Clone)]
623pub enum DropdownAction {
624 Print,
625 Custom(String),
626}
627
628#[derive(Deserialize)]
631pub struct ActorDef {
632 pub id: String,
633 pub x: f32,
634 pub y: f32,
635 pub width: f32,
636 pub height: f32,
637 pub style: Option<String>,
638 pub gif: Option<String>,
640 #[serde(default)]
641 pub z_front: bool,
642 #[serde(default = "default_true")]
643 pub return_on_end: bool,
644 #[serde(default)]
645 pub behaviours: Vec<BehaviourDef>,
646}
647
648#[derive(Deserialize)]
649pub struct BehaviourDef {
650 pub trigger: TriggerDef,
651 pub action: ActionDef,
652}
653
654#[derive(Deserialize, Clone, Copy, PartialEq, Eq)]
655pub enum TriggerDef {
656 Always,
657 OnHoverSelf,
658 OnPressSelf,
659 OnClickSelf,
660 OnClickAnywhere,
661}
662
663#[derive(Deserialize, Clone)]
664pub enum ActionDef {
665 FollowCursor {
666 speed: f32,
667 #[serde(default)]
668 trail: f32,
669 },
670 MoveTo {
671 x: f32,
672 y: f32,
673 speed: f32,
674 },
675 SwapGif {
677 path: String,
678 },
679}
680
681#[derive(Deserialize)]
684pub struct RadioGroupDef {
685 pub id: String,
686 pub x: f32,
687 pub y: f32,
688 pub width: f32,
689 pub height: f32,
690 pub options: Vec<String>,
691 #[serde(default)]
692 pub selected: usize,
693 #[serde(default)]
694 pub gap: f32,
695 pub tooltip: Option<String>,
696 pub style_idle: Option<String>,
697 pub style_selected: Option<String>,
698 pub on_change: RadioAction,
699}
700
701#[derive(Deserialize, Clone)]
702pub enum RadioAction {
703 Print,
704 Custom(String),
705}