use serde::Deserialize;
use std::sync::mpsc;
use std::time::{Duration, SystemTime};
#[derive(Debug)]
pub(crate) enum UiMsg {
SwitchRoot(String),
SwitchTabPage {
tab_id: String,
page: usize,
},
Quit,
Custom(String),
Reload,
Slider(String, f32),
TextChanged(String, String),
TextSubmitted(String, String),
Toggle(String, bool),
Dropdown(String, usize, String),
Radio(String, usize, String),
Toast {
message: String,
duration: f32,
x: f32,
y: f32,
width: f32,
height: f32,
},
}
#[derive(Deserialize)]
pub struct MenuDef {
#[serde(default)]
pub hot_reload: bool,
#[serde(default)]
pub headless_accessible: bool,
#[serde(default)]
pub shader_dirs: Vec<String>,
#[serde(default)]
pub style_dirs: Vec<String>,
pub background: Option<String>,
#[serde(default)]
pub clear_color: Option<crate::draw::Color>,
pub default_style: Option<String>,
pub start_root: String,
pub roots: Vec<RootDef>,
}
#[derive(Deserialize)]
pub struct RootDef {
pub name: String,
#[serde(default)]
pub buttons: Vec<ButtonDef>,
#[serde(default)]
pub scroll_lists: Vec<ScrollListDef>,
#[serde(default)]
pub bars: Vec<BarDef>,
#[serde(default)]
pub popouts: Vec<PopoutDef>,
#[serde(default)]
pub toggles: Vec<ToggleDef>,
#[serde(default)]
pub sliders: Vec<SliderDef>,
#[serde(default)]
pub labels: Vec<FreeLabelDef>,
#[serde(default)]
pub dividers: Vec<DividerDef>,
#[serde(default)]
pub images: Vec<ImageDef>,
#[serde(default)]
pub text_boxes: Vec<TextBoxDef>,
#[serde(default)]
pub progress_bars: Vec<ProgressBarDef>,
#[serde(default)]
pub scroll_panes: Vec<ScrollPaneDef>,
#[serde(default)]
pub dropdowns: Vec<DropdownDef>,
#[serde(default)]
pub radio_groups: Vec<RadioGroupDef>,
#[serde(default)]
pub actors: Vec<ActorDef>,
#[serde(default)]
pub tabs: Vec<TabDef>,
}
#[derive(Deserialize)]
pub struct BarDef {
pub id: String,
#[serde(default)]
pub edge: Option<BarEdgeDef>,
pub thickness: f32,
#[serde(default)]
pub pad: f32,
#[serde(default)]
pub gap: f32,
pub style: Option<String>,
pub items: Vec<BarItemDef>,
#[serde(default)]
pub manual: bool,
#[serde(default)]
pub x: f32,
#[serde(default)]
pub y: f32,
#[serde(default)]
pub width: f32,
#[serde(default)]
pub height: f32,
}
#[derive(Deserialize, PartialEq, Eq)]
pub enum BarEdgeDef {
Top,
Bottom,
Left,
Right,
Free,
}
#[derive(Deserialize)]
pub struct PopoutDef {
pub id: String,
#[serde(default)]
pub closed_x: f32,
#[serde(default)]
pub closed_y: f32,
#[serde(default)]
pub open_x: f32,
#[serde(default)]
pub open_y: f32,
pub width: f32,
pub height: f32,
pub toggle_id: String,
pub style: Option<String>,
pub edge: Option<PopoutEdgeDef>,
#[serde(default = "default_true")]
pub shadow: bool,
#[serde(default)]
pub horizontal: bool,
#[serde(default)]
pub gap: f32,
#[serde(default)]
pub full_span: bool,
#[serde(default)]
pub home_toggles: bool,
#[serde(default)]
pub items: Vec<PopoutItemDef>,
}
const fn default_true() -> bool {
true
}
#[derive(Deserialize, Clone, Copy)]
pub enum PopoutEdgeDef {
Left,
Right,
Top,
Bottom,
}
#[derive(Deserialize)]
pub enum PopoutItemDef {
Button(ButtonDef),
ScrollList(ScrollListDef),
Bar(BarDef),
Popout(PopoutDef),
}
#[derive(Deserialize)]
pub enum BarItemDef {
Button(ListButtonDef),
Label(LabelDef),
Spacer,
ScrollList(ScrollListDef),
}
#[derive(Deserialize)]
pub struct LabelDef {
pub id: String,
pub text: String,
#[serde(default = "default_label_size")]
pub size: f32,
pub color: Option<crate::draw::Color>,
#[serde(default)]
pub width: f32,
}
const fn default_label_size() -> f32 {
24.0
}
#[derive(Deserialize)]
pub struct ButtonDef {
pub id: String,
pub x: f32,
pub y: f32,
pub width: f32,
pub height: f32,
pub text: String,
pub tooltip: Option<String>,
pub style: Option<String>,
pub on_press: PressAction,
#[serde(default)]
pub nav_default: bool,
}
#[derive(Deserialize)]
pub struct ScrollListDef {
pub id: String,
pub x: f32,
pub y: f32,
pub width: f32,
pub height: f32,
#[serde(default)]
pub pad_left: f32,
#[serde(default)]
pub pad_right: f32,
#[serde(default)]
pub pad_top: f32,
#[serde(default)]
pub pad_bottom: f32,
#[serde(default)]
pub gap: f32,
pub style: Option<String>,
#[serde(default)]
pub horizontal: bool,
#[serde(default)]
pub full_span: bool,
pub items: Vec<ListButtonDef>,
}
#[derive(Deserialize)]
pub struct ListButtonDef {
pub id: String,
pub height: f32,
#[serde(default)]
pub width: f32,
pub text: String,
pub tooltip: Option<String>,
pub style: Option<String>,
pub on_press: PressAction,
}
#[derive(Deserialize, Clone)]
pub enum PressAction {
SwitchRoot(String),
SwitchTabPage { tab_id: String, page: usize },
Quit,
Print(String),
Custom(String),
Toast {
message: String,
#[serde(default = "default_toast_duration")]
duration: f32,
#[serde(default)]
x: f32,
#[serde(default)]
y: f32,
#[serde(default = "default_toast_width")]
width: f32,
#[serde(default = "default_toast_height")]
height: f32,
},
RadioSelect { group_id: String, index: usize },
DropdownSelect { dropdown_id: String, index: usize },
}
const fn default_toast_duration() -> f32 {
2.0
}
const fn default_toast_width() -> f32 {
400.0
}
const fn default_toast_height() -> f32 {
60.0
}
impl PressAction {
#[must_use]
pub const fn is_internal(&self) -> bool {
matches!(self, Self::RadioSelect { .. } | Self::DropdownSelect { .. })
}
}
#[derive(Deserialize)]
pub struct ToggleDef {
pub id: String,
pub x: f32,
pub y: f32,
pub width: f32,
pub height: f32,
pub text: String,
pub tooltip: Option<String>,
#[serde(default)]
pub checked: bool,
pub style_off: String,
pub style_on: String,
pub on_change: ToggleAction,
}
#[derive(Deserialize, Clone)]
pub enum ToggleAction {
Print,
Custom(String),
}
pub(crate) fn load_menu_soft(path: &str) -> Result<MenuDef, String> {
let src = std::fs::read_to_string(path).map_err(|e| e.to_string())?;
ron::Options::default()
.with_default_extension(
ron::extensions::Extensions::IMPLICIT_SOME
| ron::extensions::Extensions::UNWRAP_NEWTYPES,
)
.from_str(&src)
.map_err(|e| {
eprintln!("[pane_ui] Reload parse error: {e}");
e.to_string()
})
}
pub(crate) fn load_menu(path: &str) -> MenuDef {
load_menu_soft(path).unwrap_or_else(|e| panic!("[pane_ui] Failed to load '{path}': {e}"))
}
pub(crate) fn spawn_watcher(
path: String,
shader_dirs: Vec<String>,
style_dirs: Vec<String>,
tx: mpsc::Sender<UiMsg>,
) {
std::thread::spawn(move || {
let collect_stamps = || -> Vec<(String, Option<SystemTime>)> {
let mut stamps = Vec::new();
stamps.push((
path.clone(),
std::fs::metadata(&path).and_then(|m| m.modified()).ok(),
));
for dir in &shader_dirs {
if let Ok(entries) = std::fs::read_dir(dir) {
for entry in entries.filter_map(std::result::Result::ok) {
let p = entry.path();
if p.extension().is_some_and(|x| x == "wgsl") {
stamps.push((
p.to_string_lossy().to_string(),
std::fs::metadata(&p).and_then(|m| m.modified()).ok(),
));
}
}
}
}
for dir in &style_dirs {
if let Ok(entries) = std::fs::read_dir(dir) {
for entry in entries.filter_map(std::result::Result::ok) {
let p = entry.path();
if p.extension().is_some_and(|x| x == "ron") {
stamps.push((
p.to_string_lossy().to_string(),
std::fs::metadata(&p).and_then(|m| m.modified()).ok(),
));
}
}
}
}
stamps
};
let mut last_stamps = collect_stamps();
loop {
std::thread::sleep(Duration::from_secs(1));
let new_stamps = collect_stamps();
if new_stamps != last_stamps {
let _ = tx.send(UiMsg::Reload);
}
last_stamps = new_stamps;
}
});
}
#[derive(Deserialize)]
pub struct SliderDef {
pub id: String,
pub x: f32,
pub y: f32,
pub width: f32,
pub height: f32,
pub min: f32,
pub max: f32,
pub value: f32,
#[serde(default)]
pub step: Option<f32>,
pub tooltip: Option<String>,
pub style_track: String,
pub style_thumb: String,
pub on_change: SliderAction,
}
#[derive(Deserialize)]
pub struct FreeLabelDef {
pub id: String,
pub x: f32,
pub y: f32,
pub text: String,
#[serde(default = "default_label_size")]
pub size: f32,
pub color: Option<crate::draw::Color>,
#[serde(default)]
pub width: f32,
}
#[derive(Deserialize)]
pub struct DividerDef {
pub id: String,
pub x: f32,
pub y: f32,
pub width: f32,
pub height: f32,
pub style: String,
#[serde(default)]
pub full_span: bool,
}
#[derive(Deserialize)]
pub struct ImageDef {
pub id: String,
pub x: f32,
pub y: f32,
pub width: f32,
pub height: f32,
pub path: String,
#[serde(default)]
pub gif_mode: Option<crate::textures::GifMode>,
}
#[derive(Deserialize)]
pub struct TextBoxDef {
pub id: String,
pub x: f32,
pub y: f32,
pub width: f32,
pub height: f32,
#[serde(default)]
pub hint: String,
#[serde(default)]
pub max_len: Option<usize>,
pub tooltip: Option<String>,
pub style: String,
#[serde(default)]
pub style_focus: Option<String>,
pub on_change: TextBoxAction,
pub on_submit: TextBoxAction,
#[serde(default)]
pub password: bool,
#[serde(default)]
pub multiline: bool,
#[serde(default)]
pub rows: Option<u32>,
#[serde(default)]
pub font_size: Option<f32>,
}
#[derive(Deserialize)]
pub struct ProgressBarDef {
pub id: String,
pub x: f32,
pub y: f32,
pub width: f32,
pub height: f32,
pub value: f32,
pub style_track: String,
pub style_fill: String,
}
#[derive(Deserialize, Clone)]
pub enum TextBoxAction {
Print,
Custom(String),
None,
}
#[derive(Deserialize, Clone)]
pub enum SliderAction {
Print,
Custom(String),
}
#[derive(Deserialize)]
pub struct ScrollPaneDef {
pub id: String,
pub x: f32,
pub y: f32,
pub width: f32,
pub height: f32,
#[serde(default)]
pub pad_left: f32,
#[serde(default)]
pub pad_right: f32,
#[serde(default)]
pub pad_top: f32,
#[serde(default)]
pub pad_bottom: f32,
#[serde(default)]
pub gap: f32,
pub style: Option<String>,
#[serde(default)]
pub horizontal: bool,
#[serde(default)]
pub full_span: bool,
#[serde(default)]
pub manual: bool,
pub items: Vec<ContainerItemDef>,
}
#[derive(Deserialize)]
pub enum ContainerItemDef {
Button(ButtonDef),
Toggle(ToggleDef),
Slider(SliderDef),
TextBox(TextBoxDef),
Label(FreeLabelDef),
Divider(DividerDef),
Image(ImageDef),
ProgressBar(ProgressBarDef),
ScrollPane(ScrollPaneDef),
ScrollList(ScrollListDef),
Tab(TabDef),
}
#[derive(Deserialize)]
pub struct TabPageDef {
pub label: String,
pub items: Vec<ContainerItemDef>,
}
#[derive(Deserialize)]
pub struct TabDef {
pub id: String,
pub x: f32,
pub y: f32,
pub width: f32,
pub height: f32,
#[serde(default)]
pub pad_left: f32,
#[serde(default)]
pub pad_right: f32,
#[serde(default)]
pub pad_top: f32,
#[serde(default)]
pub pad_bottom: f32,
#[serde(default)]
pub gap: f32,
pub style: Option<String>,
pub pages: Vec<TabPageDef>,
#[serde(default)]
pub full_span: bool,
}
#[derive(Deserialize)]
pub struct DropdownDef {
pub id: String,
pub x: f32,
pub y: f32,
pub width: f32,
pub height: f32,
pub options: Vec<String>,
#[serde(default)]
pub selected: usize,
pub tooltip: Option<String>,
pub style: Option<String>,
pub style_list: Option<String>,
pub style_item: Option<String>,
pub on_change: DropdownAction,
}
#[derive(Deserialize, Clone)]
pub enum DropdownAction {
Print,
Custom(String),
}
#[derive(Deserialize)]
pub struct ActorDef {
pub id: String,
pub x: f32,
pub y: f32,
pub width: f32,
pub height: f32,
pub style: Option<String>,
pub gif: Option<String>,
#[serde(default)]
pub z_front: bool,
#[serde(default = "default_true")]
pub return_on_end: bool,
#[serde(default)]
pub behaviours: Vec<BehaviourDef>,
}
#[derive(Deserialize)]
pub struct BehaviourDef {
pub trigger: TriggerDef,
pub action: ActionDef,
}
#[derive(Deserialize, Clone, Copy, PartialEq, Eq)]
pub enum TriggerDef {
Always,
OnHoverSelf,
OnPressSelf,
OnClickSelf,
OnClickAnywhere,
}
#[derive(Deserialize, Clone)]
pub enum ActionDef {
FollowCursor {
speed: f32,
#[serde(default)]
trail: f32,
},
MoveTo {
x: f32,
y: f32,
speed: f32,
},
SwapGif {
path: String,
},
}
#[derive(Deserialize)]
pub struct RadioGroupDef {
pub id: String,
pub x: f32,
pub y: f32,
pub width: f32,
pub height: f32,
pub options: Vec<String>,
#[serde(default)]
pub selected: usize,
#[serde(default)]
pub gap: f32,
pub tooltip: Option<String>,
pub style_idle: Option<String>,
pub style_selected: Option<String>,
pub on_change: RadioAction,
}
#[derive(Deserialize, Clone)]
pub enum RadioAction {
Print,
Custom(String),
}