use super::events::WidgetId;
use super::style::WidgetStyle;
#[derive(Debug, Clone, Default, PartialEq)]
pub struct WidgetProps {
pub id: Option<WidgetId>,
pub visible: bool,
pub enabled: bool,
pub tooltip: Option<String>,
pub style: WidgetStyle,
}
impl WidgetProps {
pub fn new() -> Self {
Self {
id: None,
visible: true,
enabled: true,
tooltip: None,
style: WidgetStyle::default(),
}
}
pub fn with_id(mut self, id: impl Into<String>) -> Self {
self.id = Some(id.into());
self
}
pub fn with_visible(mut self, visible: bool) -> Self {
self.visible = visible;
self
}
pub fn with_enabled(mut self, enabled: bool) -> Self {
self.enabled = enabled;
self
}
pub fn with_tooltip(mut self, tooltip: impl Into<String>) -> Self {
self.tooltip = Some(tooltip.into());
self
}
pub fn with_style(mut self, style: WidgetStyle) -> Self {
self.style = style;
self
}
}
#[derive(Debug, Clone, PartialEq)]
pub enum WidgetDef {
Label { text: String, props: WidgetProps },
Button { text: String, props: WidgetProps },
TextInput { value: String, placeholder: Option<String>, password: bool, props: WidgetProps },
Checkbox { checked: bool, label: String, props: WidgetProps },
Slider { value: f32, min: f32, max: f32, step: Option<f32>, props: WidgetProps },
ProgressBar { value: f32, show_percentage: bool, props: WidgetProps },
Image { data: Vec<u8>, width: u32, height: u32, props: WidgetProps },
Separator { props: WidgetProps },
Spacer { size: f32, props: WidgetProps },
HBox { children: Vec<WidgetDef>, spacing: f32, props: WidgetProps },
VBox { children: Vec<WidgetDef>, spacing: f32, props: WidgetProps },
Grid { children: Vec<Vec<WidgetDef>>, row_spacing: f32, col_spacing: f32, props: WidgetProps },
Panel { child: Box<WidgetDef>, props: WidgetProps },
ScrollArea { child: Box<WidgetDef>, max_height: Option<f32>, props: WidgetProps },
Group { title: Option<String>, child: Box<WidgetDef>, collapsed: bool, props: WidgetProps },
Dropdown {
options: Vec<String>,
selected: Option<usize>,
placeholder: Option<String>,
props: WidgetProps,
},
RadioGroup {
options: Vec<String>,
selected: Option<usize>,
horizontal: bool,
props: WidgetProps,
},
TextArea { value: String, placeholder: Option<String>, rows: u32, props: WidgetProps },
Tabs { tabs: Vec<(String, Box<WidgetDef>)>, active: usize, props: WidgetProps },
Link { text: String, props: WidgetProps },
SelectableLabel { text: String, selected: bool, props: WidgetProps },
DragValue {
value: f64,
min: Option<f64>,
max: Option<f64>,
speed: f64,
prefix: Option<String>,
suffix: Option<String>,
decimals: Option<usize>,
props: WidgetProps,
},
ColorPicker { color: [u8; 4], alpha: bool, props: WidgetProps },
Hyperlink { text: String, url: String, new_tab: bool, props: WidgetProps },
ImageButton {
data: Vec<u8>,
width: u32,
height: u32,
frame: bool,
selected: bool,
tint: Option<[u8; 4]>,
props: WidgetProps,
},
}
impl WidgetDef {
pub fn label(text: impl Into<String>) -> Self {
Self::Label { text: text.into(), props: WidgetProps::new() }
}
pub fn button(text: impl Into<String>) -> Self {
Self::Button { text: text.into(), props: WidgetProps::new() }
}
pub fn text_input() -> Self {
Self::TextInput {
value: String::new(),
placeholder: None,
password: false,
props: WidgetProps::new(),
}
}
pub fn text_input_with_value(value: impl Into<String>) -> Self {
Self::TextInput {
value: value.into(),
placeholder: None,
password: false,
props: WidgetProps::new(),
}
}
pub fn checkbox(label: impl Into<String>, checked: bool) -> Self {
Self::Checkbox { checked, label: label.into(), props: WidgetProps::new() }
}
pub fn slider(value: f32, min: f32, max: f32) -> Self {
Self::Slider { value, min, max, step: None, props: WidgetProps::new() }
}
pub fn progress_bar(value: f32) -> Self {
Self::ProgressBar { value, show_percentage: true, props: WidgetProps::new() }
}
pub fn image(data: Vec<u8>, width: u32, height: u32) -> Self {
Self::Image { data, width, height, props: WidgetProps::new() }
}
pub fn separator() -> Self {
Self::Separator { props: WidgetProps::new() }
}
pub fn spacer(size: f32) -> Self {
Self::Spacer { size, props: WidgetProps::new() }
}
pub fn hbox(children: Vec<WidgetDef>) -> Self {
Self::HBox { children, spacing: 4.0, props: WidgetProps::new() }
}
pub fn vbox(children: Vec<WidgetDef>) -> Self {
Self::VBox { children, spacing: 4.0, props: WidgetProps::new() }
}
pub fn grid(children: Vec<Vec<WidgetDef>>) -> Self {
Self::Grid { children, row_spacing: 4.0, col_spacing: 4.0, props: WidgetProps::new() }
}
pub fn panel(child: WidgetDef) -> Self {
Self::Panel { child: Box::new(child), props: WidgetProps::new() }
}
pub fn scroll_area(child: WidgetDef) -> Self {
Self::ScrollArea { child: Box::new(child), max_height: None, props: WidgetProps::new() }
}
pub fn group(title: impl Into<String>, child: WidgetDef) -> Self {
Self::Group {
title: Some(title.into()),
child: Box::new(child),
collapsed: false,
props: WidgetProps::new(),
}
}
pub fn dropdown(options: Vec<String>) -> Self {
Self::Dropdown { options, selected: None, placeholder: None, props: WidgetProps::new() }
}
pub fn radio_group(options: Vec<String>) -> Self {
Self::RadioGroup { options, selected: None, horizontal: false, props: WidgetProps::new() }
}
pub fn text_area() -> Self {
Self::TextArea {
value: String::new(),
placeholder: None,
rows: 4,
props: WidgetProps::new(),
}
}
pub fn text_area_with_value(value: impl Into<String>) -> Self {
Self::TextArea {
value: value.into(),
placeholder: None,
rows: 4,
props: WidgetProps::new(),
}
}
pub fn tabs(tabs: Vec<(String, WidgetDef)>) -> Self {
let boxed_tabs: Vec<(String, Box<WidgetDef>)> =
tabs.into_iter().map(|(label, content)| (label, Box::new(content))).collect();
Self::Tabs { tabs: boxed_tabs, active: 0, props: WidgetProps::new() }
}
pub fn link(text: impl Into<String>) -> Self {
Self::Link { text: text.into(), props: WidgetProps::new() }
}
pub fn selectable_label(text: impl Into<String>, selected: bool) -> Self {
Self::SelectableLabel { text: text.into(), selected, props: WidgetProps::new() }
}
pub fn drag_value(value: f64) -> Self {
Self::DragValue {
value,
min: None,
max: None,
speed: 1.0,
prefix: None,
suffix: None,
decimals: None,
props: WidgetProps::new(),
}
}
pub fn color_picker(color: [u8; 4]) -> Self {
Self::ColorPicker { color, alpha: true, props: WidgetProps::new() }
}
pub fn hyperlink(text: impl Into<String>, url: impl Into<String>) -> Self {
Self::Hyperlink {
text: text.into(),
url: url.into(),
new_tab: true,
props: WidgetProps::new(),
}
}
pub fn hyperlink_url(url: impl Into<String>) -> Self {
let url_str = url.into();
Self::Hyperlink {
text: url_str.clone(),
url: url_str,
new_tab: true,
props: WidgetProps::new(),
}
}
pub fn image_button(data: Vec<u8>, width: u32, height: u32) -> Self {
Self::ImageButton {
data,
width,
height,
frame: true,
selected: false,
tint: None,
props: WidgetProps::new(),
}
}
pub fn with_id(mut self, id: impl Into<String>) -> Self {
self.props_mut().id = Some(id.into());
self
}
pub fn with_visible(mut self, visible: bool) -> Self {
self.props_mut().visible = visible;
self
}
pub fn with_enabled(mut self, enabled: bool) -> Self {
self.props_mut().enabled = enabled;
self
}
pub fn with_tooltip(mut self, tooltip: impl Into<String>) -> Self {
self.props_mut().tooltip = Some(tooltip.into());
self
}
pub fn with_style(mut self, style: WidgetStyle) -> Self {
self.props_mut().style = style;
self
}
pub fn with_font_family(mut self, family: impl Into<String>) -> Self {
self.props_mut().style.font_family = Some(family.into());
self
}
pub fn with_spacing(mut self, spacing: f32) -> Self {
match &mut self {
WidgetDef::HBox { spacing: s, .. } => *s = spacing,
WidgetDef::VBox { spacing: s, .. } => *s = spacing,
WidgetDef::Grid { row_spacing, col_spacing, .. } => {
*row_spacing = spacing;
*col_spacing = spacing;
}
_ => {}
}
self
}
pub fn with_placeholder(mut self, placeholder: impl Into<String>) -> Self {
if let WidgetDef::TextInput { placeholder: p, .. } = &mut self {
*p = Some(placeholder.into());
}
self
}
pub fn with_password(mut self, password: bool) -> Self {
if let WidgetDef::TextInput { password: p, .. } = &mut self {
*p = password;
}
self
}
pub fn with_step(mut self, step: f32) -> Self {
if let WidgetDef::Slider { step: s, .. } = &mut self {
*s = Some(step);
}
self
}
pub fn with_max_height(mut self, height: f32) -> Self {
if let WidgetDef::ScrollArea { max_height, .. } = &mut self {
*max_height = Some(height);
}
self
}
pub fn with_collapsed(mut self, collapsed: bool) -> Self {
if let WidgetDef::Group { collapsed: c, .. } = &mut self {
*c = collapsed;
}
self
}
pub fn with_selected(mut self, index: usize) -> Self {
match &mut self {
WidgetDef::Dropdown { selected, .. } => *selected = Some(index),
WidgetDef::RadioGroup { selected, .. } => *selected = Some(index),
_ => {}
}
self
}
pub fn with_horizontal(mut self, horizontal: bool) -> Self {
if let WidgetDef::RadioGroup { horizontal: h, .. } = &mut self {
*h = horizontal;
}
self
}
pub fn with_rows(mut self, rows: u32) -> Self {
if let WidgetDef::TextArea { rows: r, .. } = &mut self {
*r = rows;
}
self
}
pub fn with_active(mut self, index: usize) -> Self {
if let WidgetDef::Tabs { active, .. } = &mut self {
*active = index;
}
self
}
pub fn with_range(mut self, min: f64, max: f64) -> Self {
if let WidgetDef::DragValue { min: mi, max: ma, .. } = &mut self {
*mi = Some(min);
*ma = Some(max);
}
self
}
pub fn with_speed(mut self, speed: f64) -> Self {
if let WidgetDef::DragValue { speed: s, .. } = &mut self {
*s = speed;
}
self
}
pub fn with_prefix(mut self, prefix: impl Into<String>) -> Self {
if let WidgetDef::DragValue { prefix: p, .. } = &mut self {
*p = Some(prefix.into());
}
self
}
pub fn with_suffix(mut self, suffix: impl Into<String>) -> Self {
if let WidgetDef::DragValue { suffix: s, .. } = &mut self {
*s = Some(suffix.into());
}
self
}
pub fn with_decimals(mut self, decimals: usize) -> Self {
if let WidgetDef::DragValue { decimals: d, .. } = &mut self {
*d = Some(decimals);
}
self
}
pub fn with_alpha(mut self, alpha: bool) -> Self {
if let WidgetDef::ColorPicker { alpha: a, .. } = &mut self {
*a = alpha;
}
self
}
pub fn with_new_tab(mut self, new_tab: bool) -> Self {
if let WidgetDef::Hyperlink { new_tab: n, .. } = &mut self {
*n = new_tab;
}
self
}
pub fn with_frame(mut self, frame: bool) -> Self {
if let WidgetDef::ImageButton { frame: f, .. } = &mut self {
*f = frame;
}
self
}
pub fn with_image_selected(mut self, selected: bool) -> Self {
if let WidgetDef::ImageButton { selected: s, .. } = &mut self {
*s = selected;
}
self
}
pub fn with_tint(mut self, tint: [u8; 4]) -> Self {
if let WidgetDef::ImageButton { tint: t, .. } = &mut self {
*t = Some(tint);
}
self
}
fn props_mut(&mut self) -> &mut WidgetProps {
match self {
WidgetDef::Label { props, .. } => props,
WidgetDef::Button { props, .. } => props,
WidgetDef::TextInput { props, .. } => props,
WidgetDef::Checkbox { props, .. } => props,
WidgetDef::Slider { props, .. } => props,
WidgetDef::ProgressBar { props, .. } => props,
WidgetDef::Image { props, .. } => props,
WidgetDef::Separator { props } => props,
WidgetDef::Spacer { props, .. } => props,
WidgetDef::HBox { props, .. } => props,
WidgetDef::VBox { props, .. } => props,
WidgetDef::Grid { props, .. } => props,
WidgetDef::Panel { props, .. } => props,
WidgetDef::ScrollArea { props, .. } => props,
WidgetDef::Group { props, .. } => props,
WidgetDef::Dropdown { props, .. } => props,
WidgetDef::RadioGroup { props, .. } => props,
WidgetDef::TextArea { props, .. } => props,
WidgetDef::Tabs { props, .. } => props,
WidgetDef::Link { props, .. } => props,
WidgetDef::SelectableLabel { props, .. } => props,
WidgetDef::DragValue { props, .. } => props,
WidgetDef::ColorPicker { props, .. } => props,
WidgetDef::Hyperlink { props, .. } => props,
WidgetDef::ImageButton { props, .. } => props,
}
}
pub fn props(&self) -> &WidgetProps {
match self {
WidgetDef::Label { props, .. } => props,
WidgetDef::Button { props, .. } => props,
WidgetDef::TextInput { props, .. } => props,
WidgetDef::Checkbox { props, .. } => props,
WidgetDef::Slider { props, .. } => props,
WidgetDef::ProgressBar { props, .. } => props,
WidgetDef::Image { props, .. } => props,
WidgetDef::Separator { props } => props,
WidgetDef::Spacer { props, .. } => props,
WidgetDef::HBox { props, .. } => props,
WidgetDef::VBox { props, .. } => props,
WidgetDef::Grid { props, .. } => props,
WidgetDef::Panel { props, .. } => props,
WidgetDef::ScrollArea { props, .. } => props,
WidgetDef::Group { props, .. } => props,
WidgetDef::Dropdown { props, .. } => props,
WidgetDef::RadioGroup { props, .. } => props,
WidgetDef::TextArea { props, .. } => props,
WidgetDef::Tabs { props, .. } => props,
WidgetDef::Link { props, .. } => props,
WidgetDef::SelectableLabel { props, .. } => props,
WidgetDef::DragValue { props, .. } => props,
WidgetDef::ColorPicker { props, .. } => props,
WidgetDef::Hyperlink { props, .. } => props,
WidgetDef::ImageButton { props, .. } => props,
}
}
pub fn id(&self) -> Option<&WidgetId> {
self.props().id.as_ref()
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_label_creation() {
let label = WidgetDef::label("Hello");
if let WidgetDef::Label { text, props } = label {
assert_eq!(text, "Hello");
assert!(props.visible);
assert!(props.enabled);
} else {
panic!("Expected Label widget");
}
}
#[test]
fn test_button_with_id() {
let button = WidgetDef::button("Click").with_id("btn1");
assert_eq!(button.id(), Some(&"btn1".to_string()));
}
#[test]
fn test_vbox_with_spacing() {
let vbox =
WidgetDef::vbox(vec![WidgetDef::label("A"), WidgetDef::label("B")]).with_spacing(10.0);
if let WidgetDef::VBox { spacing, children, .. } = vbox {
assert_eq!(spacing, 10.0);
assert_eq!(children.len(), 2);
} else {
panic!("Expected VBox widget");
}
}
#[test]
fn test_text_input_with_placeholder() {
let input = WidgetDef::text_input().with_placeholder("Enter name").with_password(false);
if let WidgetDef::TextInput { placeholder, password, .. } = input {
assert_eq!(placeholder, Some("Enter name".to_string()));
assert!(!password);
} else {
panic!("Expected TextInput widget");
}
}
#[test]
fn test_nested_layout() {
let ui = WidgetDef::vbox(vec![
WidgetDef::label("Title"),
WidgetDef::hbox(vec![WidgetDef::button("OK"), WidgetDef::button("Cancel")]),
]);
if let WidgetDef::VBox { children, .. } = ui {
assert_eq!(children.len(), 2);
assert!(matches!(children[0], WidgetDef::Label { .. }));
assert!(matches!(children[1], WidgetDef::HBox { .. }));
} else {
panic!("Expected VBox widget");
}
}
}