use crate::draw::{Color, ShaderId, TextAlign};
use crate::styles::{Shape, Style, StyleId, StyleRegistry, VisualState};
use crate::textures::{GifMode, TextureId};
use serde::Deserialize;
use std::collections::HashMap;
#[derive(Deserialize)]
pub struct StyleDef {
pub shader: String,
pub idle: VisualStateDef,
pub hovered: VisualStateDef,
pub pressed: VisualStateDef,
pub disabled: VisualStateDef,
}
#[derive(Deserialize)]
pub struct VisualStateDef {
pub shape: RonShape,
pub color: Color,
pub corner_radius: f32,
pub border_width: f32,
pub border_color: Color,
pub highlight_color: Color,
pub shadow_size: f32,
pub shadow_color: Color,
pub opacity: f32,
pub scale: f32,
#[serde(default)]
pub shader: Option<String>,
#[serde(default)]
pub texture: Option<TextureDef>,
#[serde(default = "default_cast_shadow")]
pub cast_shadow: bool,
#[serde(default)]
pub text_color: Option<Color>,
#[serde(default)]
pub font_size: Option<f32>,
#[serde(default)]
pub text_align: TextAlign,
#[serde(default)]
pub text_offset_x: f32,
#[serde(default)]
pub text_offset_y: f32,
#[serde(default)]
pub font: Option<String>,
#[serde(default)]
pub bold: bool,
#[serde(default)]
pub italic: bool,
}
#[derive(Deserialize, Clone, Copy)]
pub enum RonShape {
Rectangle,
RoundedRectangle,
Pill,
Circle,
}
impl From<RonShape> for Shape {
fn from(r: RonShape) -> Self {
match r {
RonShape::Rectangle => Self::Rectangle,
RonShape::RoundedRectangle => Self::RoundedRectangle,
RonShape::Pill => Self::Pill,
RonShape::Circle => Self::Circle,
}
}
}
const fn default_cast_shadow() -> bool {
true
}
#[derive(Deserialize)]
pub struct TextureDef {
pub path: String,
#[serde(default)]
pub gif_mode: Option<GifMode>,
}
impl VisualStateDef {
fn to_visual_state(
&self,
shader_map: &HashMap<String, ShaderId>,
texture_loader: &mut dyn FnMut(&str, Option<GifMode>) -> Option<TextureId>,
fallback_shader: ShaderId,
style_name: &str,
state_name: &str,
) -> VisualState {
let shader = self.shader.as_ref().and_then(|s| if let Some(&id) = shader_map.get(s) { Some(id) } else {
eprintln!(
"[pane_ui] Style '{style_name}' state '{state_name}' references unknown shader '{s}', ignoring"
);
None
});
let texture = self.texture.as_ref().and_then(|t| {
let gif_mode = if t.path.to_lowercase().ends_with(".gif") {
Some(t.gif_mode.unwrap_or(GifMode::Loop))
} else {
None
};
texture_loader(&t.path, gif_mode)
});
VisualState {
shape: self.shape.into(),
color: self.color,
corner_radius: self.corner_radius,
border_width: self.border_width,
border_color: self.border_color,
highlight_color: self.highlight_color,
shadow_size: self.shadow_size,
shadow_color: self.shadow_color,
opacity: self.opacity,
scale: self.scale,
shader: shader.or(Some(fallback_shader)),
texture,
cast_shadow: self.cast_shadow,
text_color: self.text_color,
font_size: self.font_size,
text_align: self.text_align,
text_offset_x: self.text_offset_x,
text_offset_y: self.text_offset_y,
font: self.font.clone(),
bold: self.bold,
italic: self.italic,
}
}
}
pub const BUILTIN_STYLE_NAMES: &[&str] = &[
"plain",
"frosted_glass",
"retro",
"sharp_outline",
"glass_pill",
"emboss",
];
#[cfg(not(feature = "dev"))]
pub fn load_builtin_style(name: &str) -> String {
match name {
"plain" => include_str!("../styles/plain.ron").to_string(),
"frosted_glass" => include_str!("../styles/frosted_glass.ron").to_string(),
"retro" => include_str!("../styles/retro.ron").to_string(),
"sharp_outline" => include_str!("../styles/sharp_outline.ron").to_string(),
"glass_pill" => include_str!("../styles/glass_pill.ron").to_string(),
"emboss" => include_str!("../styles/emboss.ron").to_string(),
other => panic!("[pane_ui] Unknown built-in style '{other}'"),
}
}
#[cfg(feature = "dev")]
pub fn load_builtin_style(name: &str) -> String {
let path = format!("styles/{}.ron", name);
std::fs::read_to_string(&path)
.unwrap_or_else(|e| panic!("[pane_ui] Failed to load style '{}': {}", path, e))
}
pub fn scan_style_dir(dir: &str) -> Vec<(String, String)> {
crate::shader_reg::scan_dir(dir, "ron")
}
fn parse_and_register(
name: &str,
src: &str,
registry: &mut StyleRegistry,
shader_map: &HashMap<String, ShaderId>,
textured_id: ShaderId,
texture_loader: &mut dyn FnMut(&str, Option<GifMode>) -> Option<TextureId>,
) -> Option<(String, (StyleId, ShaderId))> {
let def: StyleDef = match ron::from_str(src) {
Ok(d) => d,
Err(e) => {
eprintln!("[pane_ui] Failed to parse style '{name}': {e}");
return None;
}
};
let Some(&fallback_shader) = shader_map.get(&def.shader) else {
eprintln!(
"[pane_ui] Style '{}' references unknown shader '{}', skipping",
name, def.shader
);
return None;
};
let style = Style {
shader: fallback_shader,
textured: textured_id,
idle: def
.idle
.to_visual_state(shader_map, texture_loader, fallback_shader, name, "idle"),
hovered: def.hovered.to_visual_state(
shader_map,
texture_loader,
fallback_shader,
name,
"hovered",
),
pressed: def.pressed.to_visual_state(
shader_map,
texture_loader,
fallback_shader,
name,
"pressed",
),
disabled: def.disabled.to_visual_state(
shader_map,
texture_loader,
fallback_shader,
name,
"disabled",
),
};
let style_id = registry.register(style);
Some((name.to_string(), (style_id, fallback_shader)))
}
pub fn register_all(
registry: &mut StyleRegistry,
shader_map: &HashMap<String, ShaderId>,
style_dirs: &[String],
textured_id: ShaderId,
texture_loader: &mut dyn FnMut(&str, Option<GifMode>) -> Option<TextureId>,
) -> HashMap<String, (StyleId, ShaderId)> {
let mut map = HashMap::new();
for name in BUILTIN_STYLE_NAMES {
let src = load_builtin_style(name);
if let Some((k, v)) = parse_and_register(
name,
&src,
registry,
shader_map,
textured_id,
texture_loader,
) {
map.insert(k, v);
}
}
for dir in style_dirs {
for (name, src) in scan_style_dir(dir) {
println!("[pane_ui] Loaded user style '{name}'");
if let Some((k, v)) = parse_and_register(
&name,
&src,
registry,
shader_map,
textured_id,
texture_loader,
) {
map.insert(k, v);
}
}
}
map
}