use std::collections::HashMap;
use crate::draw::{Color, ShaderId};
use crate::items::{Button, UiItem};
use crate::loader::{
ActionDef, ActorDef, BarDef, BarEdgeDef, BarItemDef, ButtonDef, ContainerItemDef, DropdownDef,
ListButtonDef, MenuDef, PopoutDef, PopoutEdgeDef, PopoutItemDef, ProgressBarDef, RadioGroupDef,
ScrollListDef, ScrollPaneDef, SliderDef, TabDef, ToggleDef, TriggerDef,
};
use crate::logic::Root;
use crate::styles::StyleId;
use crate::textures::{GifMode, TextureId};
pub type StyleMap = HashMap<String, (StyleId, ShaderId)>;
pub fn build_logic(
menu: &MenuDef,
style_map: &StyleMap,
textured_id: ShaderId,
tex_loader: &mut dyn FnMut(&str, Option<GifMode>) -> Option<TextureId>,
) -> (HashMap<String, Root>, Option<String>) {
let mut b = Builder::new(
style_map,
textured_id,
tex_loader,
menu.default_style.clone(),
);
let mut roots: HashMap<String, Root> = HashMap::new();
let add = |roots: &mut HashMap<String, Root>, name: &str, item: UiItem| {
roots
.entry(name.to_string())
.or_insert_with(Root::new)
.add(item);
};
for root in &menu.roots {
for btn in &root.buttons {
add(&mut roots, &root.name, UiItem::Button(b.button(btn)));
}
for list in &root.scroll_lists {
add(
&mut roots,
&root.name,
UiItem::ScrollList(b.scroll_list(list)),
);
}
let top_inset = root
.bars
.iter()
.filter(|b| b.edge.as_ref() == Some(&BarEdgeDef::Top))
.map(|b| b.thickness)
.fold(0.0_f32, f32::max);
let bottom_inset = root
.bars
.iter()
.filter(|b| b.edge.as_ref() == Some(&BarEdgeDef::Bottom))
.map(|b| b.thickness)
.fold(0.0_f32, f32::max);
for bar in &root.bars {
for item in b.bar_items(bar, top_inset, bottom_inset) {
add(&mut roots, &root.name, item);
}
}
for popout in &root.popouts {
add(&mut roots, &root.name, UiItem::Popout(b.popout(popout)));
}
for toggle in &root.toggles {
add(&mut roots, &root.name, UiItem::Toggle(b.toggle(toggle)));
}
for slider in &root.sliders {
add(&mut roots, &root.name, UiItem::Slider(b.slider(slider)));
}
for label in &root.labels {
add(
&mut roots,
&root.name,
UiItem::Label(Builder::free_label(label)),
);
}
for div in &root.dividers {
add(&mut roots, &root.name, UiItem::Divider(b.divider(div)));
}
for img in &root.images {
if let Some(item) = b.image(img) {
add(&mut roots, &root.name, UiItem::Image(item));
}
}
for tb in &root.text_boxes {
add(&mut roots, &root.name, UiItem::TextBox(b.textbox(tb)));
}
for pb in &root.progress_bars {
add(
&mut roots,
&root.name,
UiItem::ProgressBar(b.progress_bar(pb)),
);
}
for sp in &root.scroll_panes {
add(
&mut roots,
&root.name,
UiItem::ScrollPane(b.scroll_pane(sp)),
);
}
for dd in &root.dropdowns {
add(&mut roots, &root.name, UiItem::Dropdown(b.dropdown(dd)));
}
for rg in &root.radio_groups {
add(
&mut roots,
&root.name,
UiItem::RadioGroup(b.radio_group(rg)),
);
}
for actor in &root.actors {
add(&mut roots, &root.name, UiItem::Actor(b.actor(actor)));
}
for tab in &root.tabs {
add(&mut roots, &root.name, UiItem::Tab(b.tab(tab)));
}
}
let active = Some(menu.start_root.clone());
(roots, active)
}
pub struct Builder<'a> {
style_map: &'a StyleMap,
textured_id: ShaderId,
tex_loader: &'a mut dyn FnMut(&str, Option<GifMode>) -> Option<TextureId>,
default_style: Option<String>,
}
impl<'a> Builder<'a> {
pub fn new(
style_map: &'a StyleMap,
textured_id: ShaderId,
tex_loader: &'a mut dyn FnMut(&str, Option<GifMode>) -> Option<TextureId>,
default_style: Option<String>,
) -> Self {
Self {
style_map,
textured_id,
tex_loader,
default_style,
}
}
pub fn style(&self, name: &str, id: &str) -> StyleId {
if let Some(s) = self.style_map.get(name) {
return s.0;
}
if let Some(ref def) = self.default_style
&& let Some(s) = self.style_map.get(def.as_str())
{
return s.0;
}
eprintln!("[pane_ui warn] Unknown style '{name}' on '{id}', using plain");
self.style_map
.get("plain")
.expect("[pane_ui] built-in 'plain' style missing")
.0
}
pub fn style_opt(&self, name: Option<&str>, id: &str) -> StyleId {
let resolved = name.or(self.default_style.as_deref()).unwrap_or("plain");
self.style(resolved, id)
}
pub fn button(&self, def: &ButtonDef) -> Button {
Button {
id: def.id.clone(),
x: def.x,
y: def.y,
width: def.width,
height: def.height,
text: def.text.clone(),
tooltip: def.tooltip.clone(),
style: self.style_opt(def.style.as_deref(), &def.id),
action: def.on_press.clone(),
disabled: false,
nav_default: def.nav_default,
}
}
pub fn list_button(&self, def: &ListButtonDef) -> Button {
Button {
id: def.id.clone(),
x: 0.0,
y: 0.0,
width: def.width,
height: def.height,
text: def.text.clone(),
tooltip: def.tooltip.clone(),
style: self.style_opt(def.style.as_deref(), &def.id),
action: def.on_press.clone(),
disabled: false,
nav_default: false,
}
}
pub fn scroll_list(&self, def: &ScrollListDef) -> crate::items::ScrollList {
crate::items::ScrollList {
id: def.id.clone(),
x: def.x,
y: def.y,
width: def.width,
height: def.height,
pad_left: def.pad_left,
pad_right: def.pad_right,
pad_top: def.pad_top,
pad_bottom: def.pad_bottom,
gap: def.gap,
style: def.style.as_ref().map(|s| self.style(s, &def.id)),
horizontal: def.horizontal,
full_span: def.full_span,
items: def
.items
.iter()
.map(|b| {
let mut btn = self.list_button(b);
if !def.horizontal {
btn.width = def.width - def.pad_left - def.pad_right;
}
crate::items::UiItem::Button(btn)
})
.collect(),
}
}
pub fn bar_items(&self, def: &BarDef, top_inset: f32, bottom_inset: f32) -> Vec<UiItem> {
use crate::items::{Bar, BarEdge, FreeLabel};
let edge = match def.edge.as_ref().unwrap_or(&BarEdgeDef::Free) {
BarEdgeDef::Top => BarEdge::Top,
BarEdgeDef::Bottom => BarEdge::Bottom,
BarEdgeDef::Left => BarEdge::Left,
BarEdgeDef::Right => BarEdge::Right,
BarEdgeDef::Free => BarEdge::Free,
};
let items: Vec<UiItem> = def
.items
.iter()
.map(|item| match item {
BarItemDef::Button(btn) => UiItem::Button(self.list_button(btn)),
BarItemDef::Label(lbl) => UiItem::Label(FreeLabel {
id: lbl.id.clone(),
x: 0.0,
y: 0.0,
text: lbl.text.clone(),
size: lbl.size,
color: lbl.color.unwrap_or(Color::WHITE),
width: lbl.width,
}),
BarItemDef::Spacer => UiItem::Spacer,
BarItemDef::ScrollList(sl) => UiItem::ScrollList(self.scroll_list(sl)),
})
.collect();
let bar = Bar {
id: def.id.clone(),
edge,
thickness: def.thickness,
pad: def.pad,
gap: def.gap,
style: def.style.as_ref().map(|s| self.style(s, &def.id)),
top_inset,
bottom_inset,
items,
manual: def.manual,
x: def.x,
y: def.y,
width: def.width,
height: def.height,
};
vec![UiItem::Bar(bar)]
}
pub fn popout(&self, def: &PopoutDef) -> crate::items::Popout {
let items = def
.items
.iter()
.flat_map(|item| match item {
PopoutItemDef::Button(btn) => vec![UiItem::Button(self.button(btn))],
PopoutItemDef::ScrollList(list) => vec![UiItem::ScrollList(self.scroll_list(list))],
PopoutItemDef::Bar(bar) => self.bar_items(bar, 0.0, 0.0),
PopoutItemDef::Popout(inner) => vec![UiItem::Popout(self.popout(inner))],
})
.collect();
crate::items::Popout {
id: def.id.clone(),
closed_x: def.closed_x,
closed_y: def.closed_y,
open_x: def.open_x,
open_y: def.open_y,
width: def.width,
height: def.height,
toggle_id: def.toggle_id.clone(),
style: def.style.as_ref().map(|s| self.style(s, &def.id)),
edge: def.edge.map(|e| match e {
PopoutEdgeDef::Left => crate::items::PopoutEdge::Left,
PopoutEdgeDef::Right => crate::items::PopoutEdge::Right,
PopoutEdgeDef::Top => crate::items::PopoutEdge::Top,
PopoutEdgeDef::Bottom => crate::items::PopoutEdge::Bottom,
}),
shadow: def.shadow,
horizontal: def.horizontal,
gap: def.gap,
full_span: def.full_span,
home_toggles: def.home_toggles,
items,
}
}
pub fn toggle(&self, def: &ToggleDef) -> crate::items::Toggle {
crate::items::Toggle {
id: def.id.clone(),
x: def.x,
y: def.y,
width: def.width,
height: def.height,
text: def.text.clone(),
tooltip: def.tooltip.clone(),
default_checked: def.checked,
disabled: false,
style_off: self.style(&def.style_off, &def.id),
style_on: self.style(&def.style_on, &def.id),
action: def.on_change.clone(),
}
}
pub fn slider(&self, def: &SliderDef) -> crate::items::Slider {
crate::items::Slider {
id: def.id.clone(),
x: def.x,
y: def.y,
width: def.width,
height: def.height,
min: def.min,
max: def.max,
default_value: def.value,
step: def.step,
tooltip: def.tooltip.clone(),
style_track: self.style(&def.style_track, &def.id),
style_thumb: self.style(&def.style_thumb, &def.id),
action: def.on_change.clone(),
}
}
pub fn free_label(def: &crate::loader::FreeLabelDef) -> crate::items::FreeLabel {
crate::items::FreeLabel {
id: def.id.clone(),
x: def.x,
y: def.y,
text: def.text.clone(),
size: def.size,
color: def.color.unwrap_or(Color::WHITE),
width: def.width,
}
}
pub fn divider(&self, def: &crate::loader::DividerDef) -> crate::items::Divider {
crate::items::Divider {
id: def.id.clone(),
x: def.x,
y: def.y,
width: def.width,
height: def.height,
style: self.style(&def.style, &def.id),
full_span: def.full_span,
}
}
pub fn image(&mut self, def: &crate::loader::ImageDef) -> Option<crate::items::Image> {
let gif_mode = if def.path.to_lowercase().ends_with(".gif") {
Some(def.gif_mode.unwrap_or(GifMode::Loop))
} else {
None
};
let texture = (self.tex_loader)(&def.path, gif_mode)?;
Some(crate::items::Image {
id: def.id.clone(),
x: def.x,
y: def.y,
width: def.width,
height: def.height,
texture,
shader: self.textured_id,
})
}
pub fn textbox(&self, def: &crate::loader::TextBoxDef) -> crate::items::TextBox {
let style_idle = self.style(&def.style, &def.id);
let style_focus = def
.style_focus
.as_ref()
.map_or(style_idle, |s| self.style(s, &def.id));
crate::items::TextBox {
id: def.id.clone(),
x: def.x,
y: def.y,
width: def.width,
height: def.height,
default_text: String::new(),
placeholder: def.hint.clone(),
max_len: def.max_len,
tooltip: def.tooltip.clone(),
style_idle,
style_focus,
on_change: def.on_change.clone(),
on_submit: def.on_submit.clone(),
password: def.password,
multiline: def.multiline,
font_size: def.font_size,
}
}
pub fn progress_bar(&self, def: &ProgressBarDef) -> crate::items::ProgressBar {
crate::items::ProgressBar {
id: def.id.clone(),
x: def.x,
y: def.y,
width: def.width,
height: def.height,
value: def.value.clamp(0.0, 1.0),
style_track: self.style(&def.style_track, &def.id),
style_fill: self.style(&def.style_fill, &def.id),
}
}
pub fn scroll_pane(&mut self, def: &ScrollPaneDef) -> crate::items::ScrollPane {
let items = def
.items
.iter()
.filter_map(|i| self.container_item(i))
.collect();
crate::items::ScrollPane {
id: def.id.clone(),
x: def.x,
y: def.y,
width: def.width,
height: def.height,
pad_left: def.pad_left,
pad_right: def.pad_right,
pad_top: def.pad_top,
pad_bottom: def.pad_bottom,
gap: def.gap,
style: Some(self.style_opt(def.style.as_deref(), &def.id)),
horizontal: def.horizontal,
full_span: def.full_span,
manual: def.manual,
items,
}
}
fn container_item(&mut self, def: &ContainerItemDef) -> Option<UiItem> {
Some(match def {
ContainerItemDef::Button(d) => UiItem::Button(self.button(d)),
ContainerItemDef::Toggle(d) => UiItem::Toggle(self.toggle(d)),
ContainerItemDef::Slider(d) => UiItem::Slider(self.slider(d)),
ContainerItemDef::TextBox(d) => UiItem::TextBox(self.textbox(d)),
ContainerItemDef::Label(d) => UiItem::Label(Builder::free_label(d)),
ContainerItemDef::Divider(d) => UiItem::Divider(self.divider(d)),
ContainerItemDef::Image(d) => UiItem::Image(self.image(d)?),
ContainerItemDef::ProgressBar(d) => UiItem::ProgressBar(self.progress_bar(d)),
ContainerItemDef::ScrollPane(d) => UiItem::ScrollPane(self.scroll_pane(d)),
ContainerItemDef::ScrollList(d) => UiItem::ScrollList(self.scroll_list(d)),
ContainerItemDef::Tab(d) => UiItem::Tab(self.tab(d)),
})
}
pub fn tab(&mut self, def: &TabDef) -> crate::items::Tab {
let pages = def
.pages
.iter()
.map(|p| crate::items::TabPage {
label: p.label.clone(),
items: p
.items
.iter()
.filter_map(|i| self.container_item(i))
.collect(),
})
.collect();
crate::items::Tab {
id: def.id.clone(),
x: def.x,
y: def.y,
width: def.width,
height: def.height,
pad_left: def.pad_left,
pad_right: def.pad_right,
pad_top: def.pad_top,
pad_bottom: def.pad_bottom,
gap: def.gap,
style: def
.style
.as_ref()
.map(|s| self.style_opt(Some(s.as_str()), &def.id)),
pages,
full_span: def.full_span,
}
}
pub fn dropdown(&self, def: &DropdownDef) -> crate::items::Dropdown {
let selected = def.selected.min(def.options.len().saturating_sub(1));
let header_style = self.style_opt(def.style.as_deref(), &def.id);
let item_style = self.style_opt(def.style_item.as_deref(), &def.id);
let items = def
.options
.iter()
.enumerate()
.map(|(i, label)| Button {
id: format!("{}__opt_{}", def.id, i),
x: 0.0,
y: def.height * (i as f32 + 1.0),
width: def.width,
height: def.height,
text: label.clone(),
style: item_style,
tooltip: None,
action: crate::loader::PressAction::DropdownSelect {
dropdown_id: def.id.clone(),
index: i,
},
disabled: false,
nav_default: false,
})
.collect();
crate::items::Dropdown {
id: def.id.clone(),
x: def.x,
y: def.y,
width: def.width,
height: def.height,
default_selected: selected,
options: def.options.clone(),
style: header_style,
style_list: def.style_list.as_ref().map(|s| self.style(s, &def.id)),
action: def.on_change.clone(),
items,
}
}
pub fn radio_group(&self, def: &RadioGroupDef) -> crate::items::RadioGroup {
let selected = def.selected.min(def.options.len().saturating_sub(1));
let style_idle = self.style_opt(def.style_idle.as_deref(), &def.id);
let style_selected = self.style_opt(def.style_selected.as_deref(), &def.id);
let n = def.options.len();
let total_gap = def.gap * n.saturating_sub(1) as f32;
let opt_h = if n > 0 {
(def.height - total_gap) / n as f32
} else {
def.height
};
let items = def
.options
.iter()
.enumerate()
.map(|(i, label)| Button {
id: format!("{}__opt_{}", def.id, i),
x: 0.0,
y: i as f32 * (opt_h + def.gap),
width: def.width,
height: opt_h,
text: label.clone(),
style: if i == selected {
style_selected
} else {
style_idle
},
tooltip: def.tooltip.clone(),
action: crate::loader::PressAction::RadioSelect {
group_id: def.id.clone(),
index: i,
},
disabled: false,
nav_default: i == selected,
})
.collect();
crate::items::RadioGroup {
id: def.id.clone(),
default_selected: selected,
options: def.options.clone(),
style_idle,
style_selected,
action: def.on_change.clone(),
items,
x: def.x,
y: def.y,
}
}
pub fn actor(&mut self, def: &ActorDef) -> crate::items::Actor {
use crate::items::{Action, Actor, Behaviour, Trigger};
let max_trail = def
.behaviours
.iter()
.filter_map(|b| {
if let ActionDef::FollowCursor { trail, .. } = b.action {
Some(trail)
} else {
None
}
})
.fold(0.0_f32, f32::max);
let gif = def
.gif
.as_ref()
.and_then(|path| (self.tex_loader)(path, Some(crate::textures::GifMode::Loop)));
let behaviours = def
.behaviours
.iter()
.filter_map(|b| {
let trigger = match b.trigger {
TriggerDef::Always => Trigger::Always,
TriggerDef::OnHoverSelf => Trigger::OnHoverSelf,
TriggerDef::OnPressSelf => Trigger::OnPressSelf,
TriggerDef::OnClickSelf => Trigger::OnClickSelf,
TriggerDef::OnClickAnywhere => Trigger::OnClickAnywhere,
};
let action = match &b.action {
ActionDef::FollowCursor { speed, trail } => Action::FollowCursor {
speed: *speed,
trail: *trail,
},
ActionDef::MoveTo { x, y, speed } => Action::MoveTo {
x: *x,
y: *y,
speed: *speed,
},
ActionDef::SwapGif { path } => {
let texture =
(self.tex_loader)(path, Some(crate::textures::GifMode::Loop))?;
Action::SwapGif {
texture,
shader: self.textured_id,
}
}
};
Some(Behaviour { trigger, action })
})
.collect();
Actor {
id: def.id.clone(),
origin_x: def.x,
origin_y: def.y,
width: def.width,
height: def.height,
style: def.style.as_ref().map(|s| self.style(s, &def.id)),
gif,
gif_shader: self.textured_id,
z_front: def.z_front,
return_on_end: def.return_on_end,
behaviours,
trail_capacity: (max_trail + 0.5).max(0.1),
}
}
}