use crate::items::{Button, Dropdown, RadioGroup, Slider, TextBox, Toggle, UiItem};
use crate::loader::{
DropdownAction, PressAction, RadioAction, SliderAction, TextBoxAction, ToggleAction,
};
use crate::styles::StyleId;
use crate::widgets::{
ButtonState, DropdownState, ItemState, RadioGroupState, SliderState, TextBoxState,
};
pub trait WidgetBuilder {
fn build(self, default_style: Option<StyleId>) -> (UiItem, ItemState);
}
pub struct ButtonBuilder {
id: String,
x: f32,
y: f32,
w: f32,
h: f32,
text: String,
style: Option<StyleId>,
tooltip: Option<String>,
action: PressAction,
disabled: bool,
}
impl ButtonBuilder {
#[must_use]
pub fn new(id: impl Into<String>) -> Self {
Self {
id: id.into(),
x: 0.0,
y: 0.0,
w: 100.0,
h: 40.0,
text: String::new(),
style: None,
tooltip: None,
action: PressAction::Custom(String::new()),
disabled: false,
}
}
#[must_use]
pub const fn pos(mut self, x: f32, y: f32) -> Self {
self.x = x;
self.y = y;
self
}
#[must_use]
pub const fn size(mut self, w: f32, h: f32) -> Self {
self.w = w;
self.h = h;
self
}
#[must_use]
pub fn text(mut self, t: impl Into<String>) -> Self {
self.text = t.into();
self
}
#[must_use]
pub const fn style(mut self, s: StyleId) -> Self {
self.style = Some(s);
self
}
#[must_use]
pub fn tooltip(mut self, t: impl Into<String>) -> Self {
self.tooltip = Some(t.into());
self
}
#[must_use]
pub fn on_press(mut self, tag: impl Into<String>) -> Self {
self.action = PressAction::Custom(tag.into());
self
}
#[must_use]
pub const fn disabled(mut self, d: bool) -> Self {
self.disabled = d;
self
}
}
impl WidgetBuilder for ButtonBuilder {
fn build(self, default_style: Option<StyleId>) -> (UiItem, ItemState) {
let style = self.style.or(default_style).unwrap_or(StyleId::new(0));
let item = Button {
id: self.id,
x: self.x,
y: self.y,
width: self.w,
height: self.h,
text: self.text,
style,
tooltip: self.tooltip,
action: self.action,
disabled: self.disabled,
nav_default: false,
};
(UiItem::Button(item), ItemState::Button(ButtonState::new()))
}
}
pub struct ToggleBuilder {
id: String,
x: f32,
y: f32,
w: f32,
h: f32,
text: String,
style_off: Option<StyleId>,
style_on: Option<StyleId>,
tooltip: Option<String>,
action: ToggleAction,
default_checked: bool,
disabled: bool,
}
impl ToggleBuilder {
#[must_use]
pub fn new(id: impl Into<String>) -> Self {
Self {
id: id.into(),
x: 0.0,
y: 0.0,
w: 100.0,
h: 40.0,
text: String::new(),
style_off: None,
style_on: None,
tooltip: None,
action: ToggleAction::Custom(String::new()),
default_checked: false,
disabled: false,
}
}
#[must_use]
pub const fn pos(mut self, x: f32, y: f32) -> Self {
self.x = x;
self.y = y;
self
}
#[must_use]
pub const fn size(mut self, w: f32, h: f32) -> Self {
self.w = w;
self.h = h;
self
}
#[must_use]
pub fn text(mut self, t: impl Into<String>) -> Self {
self.text = t.into();
self
}
#[must_use]
pub const fn style_off(mut self, s: StyleId) -> Self {
self.style_off = Some(s);
self
}
#[must_use]
pub const fn style_on(mut self, s: StyleId) -> Self {
self.style_on = Some(s);
self
}
#[must_use]
pub const fn checked(mut self, c: bool) -> Self {
self.default_checked = c;
self
}
#[must_use]
pub fn on_change(mut self, tag: impl Into<String>) -> Self {
self.action = ToggleAction::Custom(tag.into());
self
}
}
impl WidgetBuilder for ToggleBuilder {
fn build(self, default_style: Option<StyleId>) -> (UiItem, ItemState) {
let fb = default_style.unwrap_or(StyleId::new(0));
let item = Toggle {
id: self.id,
x: self.x,
y: self.y,
width: self.w,
height: self.h,
text: self.text,
tooltip: self.tooltip,
default_checked: self.default_checked,
disabled: self.disabled,
style_off: self.style_off.unwrap_or(fb),
style_on: self.style_on.unwrap_or(fb),
action: self.action,
};
let state = ButtonState::for_toggle(&item);
(UiItem::Toggle(item), ItemState::Toggle(state))
}
}
pub struct SliderBuilder {
id: String,
x: f32,
y: f32,
w: f32,
h: f32,
min: f32,
max: f32,
default_value: f32,
step: Option<f32>,
style_track: Option<StyleId>,
style_thumb: Option<StyleId>,
tooltip: Option<String>,
action: SliderAction,
}
impl SliderBuilder {
#[must_use]
pub fn new(id: impl Into<String>) -> Self {
Self {
id: id.into(),
x: 0.0,
y: 0.0,
w: 200.0,
h: 40.0,
min: 0.0,
max: 1.0,
default_value: 0.0,
step: None,
style_track: None,
style_thumb: None,
tooltip: None,
action: SliderAction::Custom(String::new()),
}
}
#[must_use]
pub const fn pos(mut self, x: f32, y: f32) -> Self {
self.x = x;
self.y = y;
self
}
#[must_use]
pub const fn size(mut self, w: f32, h: f32) -> Self {
self.w = w;
self.h = h;
self
}
#[must_use]
pub const fn range(mut self, min: f32, max: f32) -> Self {
self.min = min;
self.max = max;
self
}
#[must_use]
pub const fn value(mut self, v: f32) -> Self {
self.default_value = v;
self
}
#[must_use]
pub const fn step(mut self, s: f32) -> Self {
self.step = Some(s);
self
}
#[must_use]
pub const fn style_track(mut self, s: StyleId) -> Self {
self.style_track = Some(s);
self
}
#[must_use]
pub const fn style_thumb(mut self, s: StyleId) -> Self {
self.style_thumb = Some(s);
self
}
#[must_use]
pub fn on_change(mut self, tag: impl Into<String>) -> Self {
self.action = SliderAction::Custom(tag.into());
self
}
}
impl WidgetBuilder for SliderBuilder {
fn build(self, default_style: Option<StyleId>) -> (UiItem, ItemState) {
let fb = default_style.unwrap_or(StyleId::new(0));
let item = Slider {
id: self.id,
x: self.x,
y: self.y,
width: self.w,
height: self.h,
min: self.min,
max: self.max,
default_value: self.default_value.clamp(self.min, self.max),
step: self.step,
tooltip: self.tooltip,
style_track: self.style_track.unwrap_or(fb),
style_thumb: self.style_thumb.unwrap_or(fb),
action: self.action,
};
let state = SliderState::for_slider(&item);
(UiItem::Slider(item), ItemState::Slider(state))
}
}
pub struct TextBoxBuilder {
id: String,
x: f32,
y: f32,
w: f32,
h: f32,
default_text: String,
placeholder: String,
max_len: Option<usize>,
style_idle: Option<StyleId>,
style_focus: Option<StyleId>,
tooltip: Option<String>,
on_change: TextBoxAction,
on_submit: TextBoxAction,
password: bool,
multiline: bool,
font_size: Option<f32>,
}
impl TextBoxBuilder {
#[must_use]
pub fn new(id: impl Into<String>) -> Self {
Self {
id: id.into(),
x: 0.0,
y: 0.0,
w: 200.0,
h: 40.0,
default_text: String::new(),
placeholder: String::new(),
max_len: None,
style_idle: None,
style_focus: None,
tooltip: None,
on_change: TextBoxAction::Custom(String::new()),
on_submit: TextBoxAction::Custom(String::new()),
password: false,
multiline: false,
font_size: None,
}
}
#[must_use]
pub const fn pos(mut self, x: f32, y: f32) -> Self {
self.x = x;
self.y = y;
self
}
#[must_use]
pub const fn size(mut self, w: f32, h: f32) -> Self {
self.w = w;
self.h = h;
self
}
#[must_use]
pub fn text(mut self, t: impl Into<String>) -> Self {
self.default_text = t.into();
self
}
#[must_use]
pub fn placeholder(mut self, t: impl Into<String>) -> Self {
self.placeholder = t.into();
self
}
#[must_use]
pub const fn max_len(mut self, n: usize) -> Self {
self.max_len = Some(n);
self
}
#[must_use]
pub const fn style(mut self, s: StyleId) -> Self {
self.style_idle = Some(s);
self
}
#[must_use]
pub const fn style_focus(mut self, s: StyleId) -> Self {
self.style_focus = Some(s);
self
}
#[must_use]
pub const fn password(mut self, p: bool) -> Self {
self.password = p;
self
}
#[must_use]
pub const fn multiline(mut self, m: bool) -> Self {
self.multiline = m;
self
}
#[must_use]
pub fn on_change(mut self, tag: impl Into<String>) -> Self {
self.on_change = TextBoxAction::Custom(tag.into());
self
}
#[must_use]
pub fn on_submit(mut self, tag: impl Into<String>) -> Self {
self.on_submit = TextBoxAction::Custom(tag.into());
self
}
}
impl WidgetBuilder for TextBoxBuilder {
fn build(self, default_style: Option<StyleId>) -> (UiItem, ItemState) {
let fb = default_style.unwrap_or(StyleId::new(0));
let style_idle = self.style_idle.unwrap_or(fb);
let item = TextBox {
id: self.id,
x: self.x,
y: self.y,
width: self.w,
height: self.h,
default_text: self.default_text,
placeholder: self.placeholder,
max_len: self.max_len,
tooltip: self.tooltip,
style_idle,
style_focus: self.style_focus.unwrap_or(style_idle),
on_change: self.on_change,
on_submit: self.on_submit,
password: self.password,
multiline: self.multiline,
font_size: self.font_size,
};
let state = TextBoxState::for_textbox(&item);
(UiItem::TextBox(item), ItemState::TextBox(state))
}
}
pub struct DropdownBuilder {
id: String,
x: f32,
y: f32,
w: f32,
h: f32,
options: Vec<String>,
default_selected: usize,
style: Option<StyleId>,
style_list: Option<StyleId>,
style_item: Option<StyleId>,
action: DropdownAction,
}
impl DropdownBuilder {
#[must_use]
pub fn new(id: impl Into<String>) -> Self {
Self {
id: id.into(),
x: 0.0,
y: 0.0,
w: 200.0,
h: 40.0,
options: Vec::new(),
default_selected: 0,
style: None,
style_list: None,
style_item: None,
action: DropdownAction::Custom(String::new()),
}
}
#[must_use]
pub const fn pos(mut self, x: f32, y: f32) -> Self {
self.x = x;
self.y = y;
self
}
#[must_use]
pub const fn size(mut self, w: f32, h: f32) -> Self {
self.w = w;
self.h = h;
self
}
#[must_use]
pub fn options<I, S>(mut self, opts: I) -> Self
where
I: IntoIterator<Item = S>,
S: Into<String>,
{
self.options = opts.into_iter().map(Into::into).collect();
self
}
#[must_use]
pub const fn selected(mut self, i: usize) -> Self {
self.default_selected = i;
self
}
#[must_use]
pub const fn style(mut self, s: StyleId) -> Self {
self.style = Some(s);
self
}
#[must_use]
pub fn on_change(mut self, tag: impl Into<String>) -> Self {
self.action = DropdownAction::Custom(tag.into());
self
}
}
impl WidgetBuilder for DropdownBuilder {
fn build(self, default_style: Option<StyleId>) -> (UiItem, ItemState) {
let fb = default_style.unwrap_or(StyleId::new(0));
let header_style = self.style.unwrap_or(fb);
let item_style = self.style_item.unwrap_or(header_style);
let n = self.options.len();
let opt_h = self.h;
let items: Vec<Button> = self
.options
.iter()
.enumerate()
.map(|(i, label)| Button {
id: format!("{}__opt_{}", self.id, i),
x: 0.0,
y: (i as f32 + 1.0) * opt_h,
width: self.w,
height: opt_h,
text: label.clone(),
style: item_style,
tooltip: None,
action: PressAction::DropdownSelect {
dropdown_id: self.id.clone(),
index: i,
},
disabled: false,
nav_default: false,
})
.collect();
let item = Dropdown {
id: self.id,
x: self.x,
y: self.y,
width: self.w,
height: self.h,
default_selected: self.default_selected.min(n.saturating_sub(1)),
options: self.options,
style: header_style,
style_list: self.style_list,
action: self.action,
items,
};
let state = DropdownState::for_dropdown(&item);
(UiItem::Dropdown(item), ItemState::Dropdown(state))
}
}
pub struct RadioGroupBuilder {
id: String,
x: f32,
y: f32,
w: f32,
h: f32,
options: Vec<String>,
default_selected: usize,
style_idle: Option<StyleId>,
style_selected: Option<StyleId>,
gap: f32,
action: RadioAction,
}
impl RadioGroupBuilder {
#[must_use]
pub fn new(id: impl Into<String>) -> Self {
Self {
id: id.into(),
x: 0.0,
y: 0.0,
w: 200.0,
h: 120.0,
options: Vec::new(),
default_selected: 0,
style_idle: None,
style_selected: None,
gap: 4.0,
action: RadioAction::Custom(String::new()),
}
}
#[must_use]
pub const fn pos(mut self, x: f32, y: f32) -> Self {
self.x = x;
self.y = y;
self
}
#[must_use]
pub const fn size(mut self, w: f32, h: f32) -> Self {
self.w = w;
self.h = h;
self
}
#[must_use]
pub fn options<I, S>(mut self, opts: I) -> Self
where
I: IntoIterator<Item = S>,
S: Into<String>,
{
self.options = opts.into_iter().map(Into::into).collect();
self
}
#[must_use]
pub const fn selected(mut self, i: usize) -> Self {
self.default_selected = i;
self
}
#[must_use]
pub const fn style_idle(mut self, s: StyleId) -> Self {
self.style_idle = Some(s);
self
}
#[must_use]
pub const fn style_selected(mut self, s: StyleId) -> Self {
self.style_selected = Some(s);
self
}
#[must_use]
pub const fn gap(mut self, g: f32) -> Self {
self.gap = g;
self
}
#[must_use]
pub fn on_change(mut self, tag: impl Into<String>) -> Self {
self.action = RadioAction::Custom(tag.into());
self
}
}
impl WidgetBuilder for RadioGroupBuilder {
fn build(self, default_style: Option<StyleId>) -> (UiItem, ItemState) {
let fb = default_style.unwrap_or(StyleId::new(0));
let style_idle = self.style_idle.unwrap_or(fb);
let style_selected = self.style_selected.unwrap_or(style_idle);
let n = self.options.len();
let total_gap = self.gap * n.saturating_sub(1) as f32;
let opt_h = if n > 0 {
(self.h - total_gap) / n as f32
} else {
self.h
};
let selected = self.default_selected.min(n.saturating_sub(1));
let items: Vec<Button> = self
.options
.iter()
.enumerate()
.map(|(i, label)| Button {
id: format!("{}__opt_{}", self.id, i),
x: 0.0,
y: i as f32 * (opt_h + self.gap),
width: self.w,
height: opt_h,
text: label.clone(),
style: if i == selected {
style_selected
} else {
style_idle
},
tooltip: None,
action: PressAction::RadioSelect {
group_id: self.id.clone(),
index: i,
},
disabled: false,
nav_default: i == selected,
})
.collect();
let item = RadioGroup {
id: self.id,
default_selected: selected,
options: self.options,
style_idle,
style_selected,
action: self.action,
items,
x: self.x,
y: self.y,
};
let state = RadioGroupState::for_radio(&item);
(UiItem::RadioGroup(item), ItemState::RadioGroup(state))
}
}