pub mod body;
pub mod content;
pub mod controls;
pub(crate) mod default_style;
pub mod div;
mod form;
pub mod validation;
pub mod widget_util;
use crate::old::registry::*;
use crate::styles::IconPlace;
use crate::widgets::body::BodyWidget;
use crate::widgets::div::DivWidget;
use crate::widgets::form::FormWidget;
use bevy::prelude::*;
use std::any::Any;
use std::fmt;
use std::sync::Arc;
pub use content::ExtendedContentWidgets;
pub use controls::ExtendedControlWidgets;
pub use validation::evaluate_validation_state;
#[derive(Component)]
pub struct IgnoreParentState;
#[derive(Resource, Default)]
pub struct ActiveScrollTarget {
pub entity: Option<Entity>,
}
#[derive(Component, Reflect, Debug, Clone)]
#[reflect(Component)]
pub struct UIGenID(usize);
impl Default for UIGenID {
fn default() -> Self {
Self(UI_ID_GENERATE.lock().unwrap().acquire())
}
}
impl UIGenID {
pub fn get(&self) -> usize {
self.0
}
}
#[derive(Component, Reflect, Debug, Clone)]
#[reflect(Component)]
pub struct BindToID(pub usize);
impl BindToID {
pub fn get(&self) -> usize {
self.0
}
}
#[derive(Component, Reflect, Default, PartialEq, Eq, Debug, Clone)]
#[reflect(Component)]
pub struct UIWidgetState {
pub focused: bool,
pub hovered: bool,
pub disabled: bool,
pub readonly: bool,
pub checked: bool,
pub open: bool,
pub invalid: bool,
}
#[derive(Component, Default, Clone, Debug, PartialEq, Eq)]
pub struct Widget(pub Option<String>);
#[derive(Component, Reflect, Debug, Clone, Default, PartialEq, Eq)]
#[reflect(Component)]
pub struct ValidationRules {
pub required: bool,
pub min_length: Option<usize>,
pub max_length: Option<usize>,
pub pattern: Option<String>,
}
impl ValidationRules {
pub fn from_attribute(value: &str) -> Option<Self> {
let mut rules = ValidationRules::default();
for part in value.split('&') {
let trimmed = part.trim();
if trimmed.is_empty() {
continue;
}
let lower = trimmed.to_ascii_lowercase();
if lower == "required" {
rules.required = true;
continue;
}
if let Some((name, args)) = trimmed.split_once('(') {
let name = name.trim().to_ascii_lowercase();
let args = args.trim_end_matches(')').trim();
match name.as_str() {
"length" => apply_length_rules(args, &mut rules),
"pattern" => apply_pattern_rule(args, &mut rules),
_ => {}
}
}
}
if rules.is_empty() { None } else { Some(rules) }
}
fn is_empty(&self) -> bool {
!self.required
&& self.min_length.is_none()
&& self.max_length.is_none()
&& self.pattern.is_none()
}
}
fn apply_length_rules(args: &str, rules: &mut ValidationRules) {
let parts: Vec<&str> = args.split(',').map(|part| part.trim()).collect();
if parts.is_empty() {
return;
}
let parse_part = |part: &str| part.parse::<usize>().ok();
match parts.as_slice() {
[single] => {
if let Some(value) = parse_part(single) {
rules.min_length = Some(value);
rules.max_length = Some(value);
}
}
[min, max, ..] => {
if let Some(value) = parse_part(min) {
rules.min_length = Some(value);
}
if let Some(value) = parse_part(max) {
rules.max_length = Some(value);
}
}
&[] => {}
}
}
fn apply_pattern_rule(args: &str, rules: &mut ValidationRules) {
let trimmed = args.trim();
let stripped = trimmed
.strip_prefix('"')
.and_then(|rest| rest.strip_suffix('"'))
.or_else(|| {
trimmed
.strip_prefix('\'')
.and_then(|rest| rest.strip_suffix('\''))
})
.unwrap_or(trimmed);
if stripped.is_empty() {
return;
}
rules.pattern = Some(stripped.to_string());
}
#[derive(Component, Clone, Copy, Debug)]
pub struct WidgetId {
pub id: usize,
pub kind: WidgetKind,
}
#[derive(Debug, Clone, Copy)]
pub enum WidgetKind {
Body,
Button,
ColorPicker,
CheckBox,
ChoiceBox,
DatePicker,
Div,
Divider,
Form,
FieldSet,
Headline,
HyperLink,
Img,
InputField,
Paragraph,
ToolTip,
Badge,
ProgressBar,
RadioButton,
Scrollbar,
Slider,
SwitchButton,
ToggleButton,
ListBox,
}
pub struct ExtendedWidgetPlugin;
impl Plugin for ExtendedWidgetPlugin {
fn build(&self, app: &mut App) {
app.init_resource::<ActiveScrollTarget>();
app.register_type::<UIGenID>();
app.register_type::<BindToID>();
app.register_type::<UIWidgetState>();
app.register_type::<ValidationRules>();
app.register_type::<Body>();
app.register_type::<Form>();
app.add_plugins((
ExtendedControlWidgets,
ExtendedContentWidgets,
BodyWidget,
DivWidget,
FormWidget,
));
app.add_systems(Update, validation::update_validation_states);
}
}
#[derive(Component, Reflect, Debug, Clone, PartialEq, Eq)]
#[reflect(Component)]
#[require(UIGenID, UIWidgetState, GlobalTransform, InheritedVisibility, Widget)]
pub struct Body {
pub entry: usize,
pub html_key: Option<String>,
}
impl Default for Body {
fn default() -> Self {
let entry = BODY_ID_POOL.lock().unwrap().acquire();
Self {
entry,
html_key: None,
}
}
}
#[derive(Component, Reflect, Debug, Clone, PartialEq, Eq)]
#[reflect(Component)]
#[require(UIGenID, UIWidgetState, GlobalTransform, InheritedVisibility, Widget)]
pub struct Div(pub usize);
impl Default for Div {
fn default() -> Self {
let entry = DIV_ID_POOL.lock().unwrap().acquire();
Self(entry)
}
}
#[derive(Component, Reflect, Debug, Clone, PartialEq, Eq)]
#[reflect(Component)]
#[require(UIGenID, UIWidgetState, GlobalTransform, InheritedVisibility, Widget)]
pub struct Form {
pub entry: usize,
pub action: Option<String>,
pub validate_mode: FormValidationMode,
}
impl Default for Form {
fn default() -> Self {
let entry = FORM_ID_POOL.lock().unwrap().acquire();
Self {
entry,
action: None,
validate_mode: FormValidationMode::default(),
}
}
}
#[derive(Reflect, Default, Debug, Clone, Eq, PartialEq)]
pub enum FormValidationMode {
Always,
#[default]
Send,
Interact,
}
impl FormValidationMode {
pub fn from_str(value: &str) -> Option<FormValidationMode> {
match value.trim().to_ascii_lowercase().as_str() {
"always" | "all" => Some(FormValidationMode::Always),
"send" => Some(FormValidationMode::Send),
"interact" => Some(FormValidationMode::Interact),
_ => None,
}
}
}
#[derive(Reflect, Default, Debug, Clone, Eq, PartialEq)]
pub enum ButtonType {
#[default]
Auto,
Button,
Submit,
Reset,
}
impl ButtonType {
pub fn from_str(value: &str) -> Option<ButtonType> {
match value.to_ascii_lowercase().as_str() {
"button" => Some(ButtonType::Button),
"submit" => Some(ButtonType::Submit),
"reset" => Some(ButtonType::Reset),
_ => None,
}
}
}
#[derive(Component, Reflect, Debug, Clone, PartialEq, Eq)]
#[reflect(Component)]
#[require(UIGenID, UIWidgetState, Widget)]
pub struct Button {
pub entry: usize,
pub text: String,
pub icon_place: IconPlace,
pub icon_path: Option<String>,
pub button_type: ButtonType,
}
impl Default for Button {
fn default() -> Self {
let entry = BUTTON_ID_POOL.lock().unwrap().acquire();
Self {
entry,
text: String::from("Button"),
icon_path: None,
icon_place: IconPlace::default(),
button_type: ButtonType::default(),
}
}
}
#[derive(Component, Reflect, Debug, Clone, PartialEq, Eq)]
#[reflect(Component)]
#[require(UIGenID, UIWidgetState, Widget)]
pub struct CheckBox {
pub entry: usize,
pub label: String,
pub icon_path: Option<String>,
pub checked: bool,
}
impl Default for CheckBox {
fn default() -> Self {
let entry = CHECK_BOX_ID_POOL.lock().unwrap().acquire();
Self {
entry,
label: String::from("label"),
icon_path: Some(String::from("extended_ui/icons/check-mark.png")),
checked: false,
}
}
}
#[derive(Component, Reflect, Debug, Clone, PartialEq, Eq)]
#[reflect(Component)]
#[require(UIGenID, UIWidgetState, Widget)]
pub struct ChoiceBox {
pub entry: usize,
pub label: String,
pub value: ChoiceOption,
pub options: Vec<ChoiceOption>,
pub icon_path: Option<String>,
}
impl Default for ChoiceBox {
fn default() -> Self {
let entry = CHOICE_BOX_ID_POOL.lock().unwrap().acquire();
Self {
entry,
label: String::from("select"),
value: ChoiceOption::default(),
options: vec![ChoiceOption::default()],
icon_path: Some(String::from("extended_ui/icons/drop-arrow.png")),
}
}
}
#[derive(Component, Reflect, Debug, Clone)]
pub struct ChoiceOption {
pub text: String,
#[reflect(ignore)]
pub value: WidgetValue,
pub icon_path: Option<String>,
}
impl PartialEq for ChoiceOption {
fn eq(&self, other: &Self) -> bool {
if self.text != other.text || self.icon_path != other.icon_path {
return false;
}
match (&self.value.0, &other.value.0) {
(None, None) => true,
(Some(a), Some(b)) => match (a.downcast_ref::<String>(), b.downcast_ref::<String>()) {
(Some(sa), Some(sb)) => sa == sb,
_ => Arc::ptr_eq(a, b),
},
_ => false,
}
}
}
impl Eq for ChoiceOption {}
impl Default for ChoiceOption {
fn default() -> Self {
Self {
text: String::from("Please Select"),
value: WidgetValue::new(String::from("default")),
icon_path: None,
}
}
}
impl ChoiceOption {
pub fn new(text: &str) -> Self {
Self {
text: text.to_string(),
value: WidgetValue::new(text.trim().to_string()),
icon_path: None,
}
}
pub fn with_value<T: Any + Send + Sync>(mut self, value: T) -> Self {
self.value.set(value);
self
}
pub fn get_value<T: Any>(&self) -> Option<&T> {
self.value.get::<T>()
}
pub fn value_as_str(&self) -> Option<&str> {
self.value.as_str()
}
pub fn get_reflected(&self) -> Option<&ReflectedValue> {
self.value.reflect()
}
}
pub struct ReflectedValue(pub Box<dyn PartialReflect>);
impl ReflectedValue {
pub fn downcast_ref<T: 'static>(&self) -> Option<&T> {
self.0.try_as_reflect()?.as_any().downcast_ref::<T>()
}
}
#[derive(Component, Reflect, Debug, Clone, PartialEq, Eq)]
#[reflect(Component)]
#[require(UIGenID, UIWidgetState, Widget)]
pub struct ListBox {
pub entry: usize,
pub options: Vec<ChoiceOption>,
pub values: Vec<ChoiceOption>,
pub multiselect: bool,
}
impl Default for ListBox {
fn default() -> Self {
let entry = LIST_BOX_ID_POOL.lock().unwrap().acquire();
Self {
entry,
options: vec![
ChoiceOption::new("Option A"),
ChoiceOption::new("Option B"),
ChoiceOption::new("Option C"),
],
values: Vec::new(),
multiselect: false,
}
}
}
#[derive(Component, Reflect, Debug, Clone, PartialEq, Eq)]
#[reflect(Component)]
#[require(UIGenID, UIWidgetState, Widget)]
pub struct Divider {
pub entry: usize,
pub alignment: DividerAlignment,
}
impl Default for Divider {
fn default() -> Self {
let entry = DIVIDER_ID_POOL.lock().unwrap().acquire();
Self {
entry,
alignment: DividerAlignment::default(),
}
}
}
#[derive(Reflect, Default, Debug, Clone, Eq, PartialEq)]
pub enum DividerAlignment {
#[default]
Vertical,
Horizontal,
}
impl DividerAlignment {
pub fn from_str(s: &str) -> Option<Self> {
match s.trim().to_ascii_lowercase().as_str() {
"vertical" | "vert" | "v" => Some(Self::Vertical),
"horizontal" | "horiz" | "h" => Some(Self::Horizontal),
_ => None,
}
}
}
impl fmt::Display for DividerAlignment {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
let s = match self {
DividerAlignment::Horizontal => "horizontal",
DividerAlignment::Vertical => "vertical",
};
write!(f, "{}", s)
}
}
#[derive(Component, Reflect, Debug, Clone, PartialEq, Eq)]
#[reflect(Component)]
#[require(UIGenID, UIWidgetState, Widget)]
pub struct FieldSet {
pub entry: usize,
pub kind: Option<FieldKind>,
pub field_mode: FieldMode,
pub allow_none: bool,
}
impl Default for FieldSet {
fn default() -> Self {
let entry = FIELDSET_ID_POOL.lock().unwrap().acquire();
Self {
entry,
kind: None,
field_mode: FieldMode::Single,
allow_none: false,
}
}
}
#[derive(Reflect, Debug, Clone, Copy, PartialEq, Eq)]
pub enum FieldKind {
Radio,
Toggle,
}
#[derive(Reflect, Debug, Clone, Copy, PartialEq, Eq)]
pub enum FieldMode {
Multi,
Single,
Count(u8),
}
impl FieldMode {
pub fn from_str(s: &str) -> Option<Self> {
let normalized = s.trim().to_ascii_lowercase();
if let Some(inner) = normalized
.strip_prefix("count(")
.and_then(|rest| rest.strip_suffix(')'))
{
let value = inner.trim();
if value.is_empty() {
return Some(Self::Count(0));
}
return value.parse::<u8>().ok().map(Self::Count);
}
match normalized.as_str() {
"single" | "solo" | "one" => Some(Self::Single),
"multi" | "more" => Some(Self::Multi),
"count" => Some(Self::Count(0)),
_ => None,
}
}
}
#[derive(Component, Reflect, Debug)]
#[reflect(Component)]
pub struct InFieldSet(pub Entity);
#[derive(Component, Reflect, Debug, Default)]
pub struct FieldSelectionSingle(pub Option<Entity>);
#[derive(Component, Reflect, Debug, Default)]
pub struct FieldSelectionMulti(pub Vec<Entity>);
#[derive(Component, Reflect, Debug, Clone, PartialEq, Eq)]
#[reflect(Component)]
#[require(UIGenID, UIWidgetState, Widget)]
pub struct Headline {
pub entry: usize,
pub text: String,
pub h_type: HeadlineType,
}
impl Default for Headline {
fn default() -> Self {
let entry = HEADLINE_ID_POOL.lock().unwrap().acquire();
Self {
entry,
text: String::from("Headline"),
h_type: HeadlineType::H3,
}
}
}
#[derive(Reflect, Default, Debug, Clone, Eq, PartialEq)]
pub enum HeadlineType {
#[default]
H1,
H2,
H3,
H4,
H5,
H6,
}
impl fmt::Display for HeadlineType {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
let s = match self {
HeadlineType::H1 => "h1",
HeadlineType::H2 => "h2",
HeadlineType::H3 => "h3",
HeadlineType::H4 => "h4",
HeadlineType::H5 => "h5",
HeadlineType::H6 => "h6",
};
write!(f, "{}", s)
}
}
#[derive(Component, Reflect, Debug, Clone, PartialEq, Eq)]
#[reflect(Component)]
#[require(UIGenID, UIWidgetState, GlobalTransform, InheritedVisibility, Widget)]
pub struct Img {
pub entry: usize,
pub src: Option<String>,
pub alt: String,
pub preview: Option<String>,
}
impl Default for Img {
fn default() -> Self {
let entry = IMAGE_ID_POOL.lock().unwrap().acquire();
Self {
entry,
src: None,
alt: String::from(""),
preview: None,
}
}
}
#[derive(Reflect, Default, Debug, Clone, Copy, Eq, PartialEq)]
pub enum DateFormat {
#[default]
MonthDayYear,
DayMonthYear,
YearMonthDay,
}
impl DateFormat {
pub fn from_str(value: &str) -> Option<DateFormat> {
match value.trim().to_ascii_lowercase().as_str() {
"mdy" | "mm/dd/yyyy" | "mm-dd-yyyy" | "mm.dd.yyyy" | "month-day-year" => {
Some(DateFormat::MonthDayYear)
}
"dmy" | "dd/mm/yyyy" | "dd-mm-yyyy" | "dd.mm.yyyy" | "day-month-year" => {
Some(DateFormat::DayMonthYear)
}
"ymd" | "yyyy-mm-dd" | "yyyy/mm/dd" | "yyyy.mm.dd" | "year-month-day" | "iso" => {
Some(DateFormat::YearMonthDay)
}
_ => None,
}
}
}
#[derive(Component, Reflect, Debug, Clone, PartialEq)]
#[reflect(Component)]
#[require(UIGenID, UIWidgetState, Widget, InputValue)]
pub struct DatePicker {
pub entry: usize,
pub for_id: Option<String>,
pub name: String,
pub label: String,
pub placeholder: String,
pub value: String,
pub min: Option<String>,
pub max: Option<String>,
pub format_pattern: Option<String>,
pub format: DateFormat,
}
impl Default for DatePicker {
fn default() -> Self {
let entry = DATE_PICKER_ID_POOL.lock().unwrap().acquire();
Self {
entry,
for_id: None,
name: String::new(),
label: String::from("Date"),
placeholder: String::new(),
value: String::new(),
min: None,
max: None,
format_pattern: None,
format: DateFormat::default(),
}
}
}
#[derive(Component, Reflect, Debug, Clone, PartialEq)]
#[reflect(Component)]
#[require(UIGenID, UIWidgetState, Widget, InputValue)]
pub struct InputField {
pub entry: usize,
pub name: String,
pub text: String,
pub label: String,
pub placeholder: String,
pub cursor_position: usize,
pub clear_after_focus_lost: bool,
pub icon_path: Option<String>,
pub input_type: InputType,
pub date_format: Option<String>,
pub folder: bool,
pub extensions: Vec<String>,
pub show_size: bool,
pub max_size_bytes: Option<u64>,
pub cap_text_at: InputCap,
}
impl Default for InputField {
fn default() -> Self {
let entry = INPUT_ID_POOL.lock().unwrap().acquire();
Self {
entry,
name: String::new(),
text: String::from(""),
label: String::from("Label"),
placeholder: String::from(""),
clear_after_focus_lost: false,
cursor_position: 0,
icon_path: None,
cap_text_at: InputCap::default(),
input_type: InputType::default(),
date_format: None,
folder: false,
extensions: Vec::new(),
show_size: false,
max_size_bytes: None,
}
}
}
#[derive(Reflect, Default, Debug, Clone, Copy, Eq, PartialEq)]
pub enum InputType {
#[default]
Text,
Email,
Date,
Range,
Password,
Number,
File,
}
impl InputType {
pub fn is_valid_char(&self, c: char) -> bool {
match self {
InputType::Text | InputType::Password => true,
InputType::Number => c.is_ascii_digit() || "+-*/()., ".contains(c),
InputType::Email => c.is_ascii_alphanumeric() || c == '@' || c == '.' || c == '-',
InputType::Date => c.is_ascii_digit() || c == '/' || c == '-' || c == '.',
InputType::Range => c.is_ascii_digit() || c == '/' || c == '-' || c == '.' || c == ' ',
InputType::File => false,
}
}
pub fn from_str(value: &str) -> Option<InputType> {
match value.to_lowercase().as_str() {
"text" => Some(InputType::Text),
"password" => Some(InputType::Password),
"number" => Some(InputType::Number),
"email" => Some(InputType::Email),
"date" => Some(InputType::Date),
"range" => Some(InputType::Range),
"file" => Some(InputType::File),
_ => None,
}
}
}
#[derive(Reflect, Default, Debug, Clone, Eq, PartialEq)]
pub enum InputCap {
#[default]
NoCap,
CapAtNodeSize,
CapAt(usize), }
impl InputCap {
pub fn get_value(&self) -> usize {
match self {
Self::CapAt(value) => *value,
Self::NoCap => 0,
Self::CapAtNodeSize => 0,
}
}
}
#[derive(Component, Reflect, Debug, Clone, Default)]
#[reflect(Component)]
pub struct InputValue(pub String);
#[derive(Reflect, Debug, Clone, Eq, PartialEq, Default)]
pub enum HyperLinkBrowsers {
#[default]
System,
Custom(Vec<String>),
}
impl HyperLinkBrowsers {
pub fn from_str(value: &str) -> Option<Self> {
let trimmed = value.trim();
if trimmed.is_empty() {
return Some(Self::System);
}
if trimmed.eq_ignore_ascii_case("system") {
return Some(Self::System);
}
let parsed = if let Some(inner) = trimmed
.strip_prefix('[')
.and_then(|raw| raw.strip_suffix(']'))
{
inner
.split(',')
.map(|entry| normalize_browser_name(entry))
.filter(|entry| !entry.is_empty())
.collect::<Vec<_>>()
} else {
let single = normalize_browser_name(trimmed);
if single.is_empty() {
Vec::new()
} else if single.eq_ignore_ascii_case("system") {
return Some(Self::System);
} else {
vec![single]
}
};
if parsed.is_empty() {
Some(Self::System)
} else {
Some(Self::Custom(parsed))
}
}
}
fn normalize_browser_name(value: &str) -> String {
value
.trim()
.trim_matches('"')
.trim_matches('\'')
.trim()
.to_ascii_lowercase()
}
#[derive(Component, Reflect, Debug, Clone, PartialEq, Eq)]
#[reflect(Component)]
#[require(UIGenID, UIWidgetState, Widget)]
pub struct HyperLink {
pub entry: usize,
pub text: String,
pub href: String,
pub browsers: HyperLinkBrowsers,
pub open_modal: bool,
}
impl Default for HyperLink {
fn default() -> Self {
let entry = HYPER_LINK_ID_POOL.lock().unwrap().acquire();
Self {
entry,
text: String::new(),
href: String::new(),
browsers: HyperLinkBrowsers::default(),
open_modal: false,
}
}
}
#[derive(Component, Reflect, Debug, Clone, PartialEq, Eq)]
#[reflect(Component)]
#[require(UIGenID, UIWidgetState, Widget)]
pub struct Paragraph {
pub entry: usize,
pub text: String,
}
impl Default for Paragraph {
fn default() -> Self {
let entry = PARAGRAPH_ID_POOL.lock().unwrap().acquire();
Self {
entry,
text: String::from(""),
}
}
}
#[derive(Reflect, Default, Debug, Clone, Copy, Eq, PartialEq)]
pub enum BadgeAnchor {
TopLeft,
#[default]
TopRight,
BottomLeft,
BottomRight,
}
impl BadgeAnchor {
pub fn from_str(value: &str) -> Option<Self> {
let normalized = value.to_ascii_lowercase().replace(['-', '_'], " ");
let mut vertical = None;
let mut horizontal = None;
for token in normalized.split([' ', ',', '|', '/']) {
let token = token.trim();
if token.is_empty() {
continue;
}
match token {
"top" => vertical = Some("top"),
"bottom" => vertical = Some("bottom"),
"left" => horizontal = Some("left"),
"right" => horizontal = Some("right"),
_ => {}
}
}
if vertical.is_none() && horizontal.is_none() {
return None;
}
Some(match (vertical, horizontal) {
(Some("top"), Some("left")) => Self::TopLeft,
(Some("top"), Some("right")) => Self::TopRight,
(Some("bottom"), Some("left")) => Self::BottomLeft,
(Some("bottom"), Some("right")) => Self::BottomRight,
(Some("top"), None) => Self::TopRight,
(Some("bottom"), None) => Self::BottomRight,
(None, Some("left")) => Self::TopLeft,
(None, Some("right")) => Self::TopRight,
_ => Self::TopRight,
})
}
}
#[derive(Component, Reflect, Debug, Clone, PartialEq, Eq)]
#[reflect(Component)]
#[require(UIGenID, UIWidgetState, Widget)]
pub struct Badge {
pub entry: usize,
pub value: u32,
pub max: u32,
pub for_id: Option<String>,
pub anchor: BadgeAnchor,
}
impl Default for Badge {
fn default() -> Self {
let entry = BADGE_ID_POOL.lock().unwrap().acquire();
Self {
entry,
value: 0,
max: 99,
for_id: None,
anchor: BadgeAnchor::default(),
}
}
}
#[derive(Reflect, Default, Debug, Clone, Copy, Eq, PartialEq)]
pub enum ToolTipVariant {
#[default]
Follow,
Point,
}
impl ToolTipVariant {
pub fn from_str(value: &str) -> Option<Self> {
match value.trim().to_ascii_lowercase().as_str() {
"follow" => Some(Self::Follow),
"point" => Some(Self::Point),
_ => None,
}
}
}
#[derive(Reflect, Default, Debug, Clone, Copy, Eq, PartialEq)]
pub enum ToolTipPriority {
Top,
Bottom,
Left,
#[default]
Right,
}
impl ToolTipPriority {
pub fn from_str(value: &str) -> Option<Self> {
match value.trim().to_ascii_lowercase().as_str() {
"top" => Some(Self::Top),
"bottom" => Some(Self::Bottom),
"left" => Some(Self::Left),
"right" => Some(Self::Right),
_ => None,
}
}
}
#[derive(Reflect, Default, Debug, Clone, Copy, Eq, PartialEq)]
pub enum ToolTipAlignment {
Vertical,
#[default]
Horizontal,
}
impl ToolTipAlignment {
pub fn from_str(value: &str) -> Option<Self> {
match value.trim().to_ascii_lowercase().as_str() {
"vertical" => Some(Self::Vertical),
"horizontal" => Some(Self::Horizontal),
_ => None,
}
}
}
#[derive(Reflect, Debug, Clone, Copy, Eq, PartialEq)]
pub enum ToolTipTrigger {
Hover,
Click,
Drag,
}
impl ToolTipTrigger {
pub fn from_str(value: &str) -> Option<Self> {
match value.trim().to_ascii_lowercase().as_str() {
"hover" => Some(Self::Hover),
"click" => Some(Self::Click),
"drag" => Some(Self::Drag),
_ => None,
}
}
}
#[derive(Component, Reflect, Debug, Clone, PartialEq, Eq)]
#[reflect(Component)]
#[require(UIGenID, UIWidgetState, Widget)]
pub struct ToolTip {
pub entry: usize,
pub text: String,
pub for_id: Option<String>,
pub variant: ToolTipVariant,
pub prio: ToolTipPriority,
pub alignment: ToolTipAlignment,
pub trigger: Vec<ToolTipTrigger>,
}
impl Default for ToolTip {
fn default() -> Self {
let entry = TOOL_TIP_ID_POOL.lock().unwrap().acquire();
Self {
entry,
text: String::new(),
for_id: None,
variant: ToolTipVariant::default(),
prio: ToolTipPriority::default(),
alignment: ToolTipAlignment::default(),
trigger: vec![ToolTipTrigger::Hover],
}
}
}
#[derive(Component, Reflect, Debug, Clone, PartialEq)]
#[reflect(Component)]
#[require(UIGenID, UIWidgetState, InheritedVisibility, Widget)]
pub struct ProgressBar {
pub entry: usize,
pub value: f32,
pub min: f32,
pub max: f32,
}
impl Default for ProgressBar {
fn default() -> Self {
let entry = PROGRESS_BAR_ID_POOL.lock().unwrap().acquire();
Self {
entry,
value: 0.0,
max: 100.0,
min: 0.0,
}
}
}
#[derive(Component, Reflect, Debug, Clone, PartialEq)]
#[reflect(Component)]
#[require(UIGenID, UIWidgetState, Widget)]
pub struct RadioButton {
pub entry: usize,
pub label: String,
#[reflect(ignore)]
pub value: WidgetValue,
pub selected: bool,
}
impl Default for RadioButton {
fn default() -> Self {
let entry = RADIO_BUTTON_ID_POOL.lock().unwrap().acquire();
Self {
entry,
label: String::from("label"),
value: WidgetValue::new(String::new()),
selected: false,
}
}
}
impl RadioButton {
pub fn with_value<T: Any + Send + Sync>(mut self, value: T) -> Self {
self.value.set(value);
self
}
pub fn get_value<T: Any>(&self) -> Option<&T> {
self.value.get::<T>()
}
pub fn value_as_str(&self) -> Option<&str> {
self.value.as_str()
}
pub fn get_reflected(&self) -> Option<&ReflectedValue> {
self.value.reflect()
}
}
#[derive(Debug, Clone)]
pub struct WidgetValue(Option<Arc<dyn Any + Send + Sync>>);
impl PartialEq for WidgetValue {
fn eq(&self, other: &Self) -> bool {
match (&self.0, &other.0) {
(None, None) => true,
(Some(a), Some(b)) => match (a.downcast_ref::<String>(), b.downcast_ref::<String>()) {
(Some(sa), Some(sb)) => sa == sb,
_ => Arc::ptr_eq(a, b),
},
_ => false,
}
}
}
impl Eq for WidgetValue {}
impl Default for WidgetValue {
fn default() -> Self {
Self(None)
}
}
impl WidgetValue {
pub fn new<T: Any + Send + Sync>(value: T) -> Self {
Self(Some(Arc::new(value)))
}
pub fn get<T: Any>(&self) -> Option<&T> {
self.0.as_ref()?.downcast_ref::<T>()
}
pub fn set<T: Any + Send + Sync>(&mut self, value: T) {
self.0 = Some(Arc::new(value));
}
pub fn as_str(&self) -> Option<&str> {
self.0
.as_ref()?
.downcast_ref::<String>()
.map(|s| s.as_str())
}
pub fn reflect(&self) -> Option<&ReflectedValue> {
self.get::<ReflectedValue>()
}
}
#[derive(Component, Reflect, Debug, Clone, PartialEq)]
#[reflect(Component)]
#[require(UIGenID, UIWidgetState, Widget)]
pub struct Scrollbar {
pub entry: usize,
pub entity: Option<Entity>,
pub value: f32, pub min: f32,
pub max: f32,
pub step: f32,
pub vertical: bool,
pub viewport_extent: f32,
pub content_extent: f32,
}
impl Default for Scrollbar {
fn default() -> Self {
let entry = SCROLL_ID_POOL.lock().unwrap().acquire();
Self {
entry,
entity: None,
value: 0.0,
min: 0.0,
max: 1000.0,
step: 10.0,
vertical: true,
viewport_extent: 0.0,
content_extent: 0.0,
}
}
}
#[derive(Reflect, Default, Debug, Clone, Copy, Eq, PartialEq)]
pub enum SliderType {
#[default]
Default,
Range,
}
impl SliderType {
pub fn from_str(value: &str) -> Option<Self> {
match value.trim().to_ascii_lowercase().as_str() {
"default" => Some(Self::Default),
"range" => Some(Self::Range),
_ => None,
}
}
}
#[derive(Reflect, Default, Debug, Clone, Copy, Eq, PartialEq)]
pub enum SliderDotAnchor {
#[default]
Top,
Bottom,
}
impl SliderDotAnchor {
pub fn from_str(value: &str) -> Option<Self> {
match value.trim().to_ascii_lowercase().as_str() {
"top" => Some(Self::Top),
"bottom" => Some(Self::Bottom),
_ => None,
}
}
}
#[derive(Component, Reflect, Debug, Clone, PartialEq)]
#[reflect(Component)]
#[require(UIGenID, UIWidgetState, Widget)]
pub struct Slider {
pub entry: usize,
pub slider_type: SliderType,
pub value: f32,
pub range_start: f32,
pub range_end: f32,
pub step: f32,
pub min: f32,
pub max: f32,
pub dots: Option<u32>,
pub show_labels: bool,
pub show_tip: bool,
pub dot_anchor: SliderDotAnchor,
}
impl Default for Slider {
fn default() -> Self {
let entry = SLIDER_ID_POOL.lock().unwrap().acquire();
Self {
entry,
slider_type: SliderType::Default,
value: 0.0,
range_start: 20.0,
range_end: 40.0,
step: 1.0,
min: 0.0,
max: 100.0,
dots: None,
show_labels: false,
show_tip: true,
dot_anchor: SliderDotAnchor::Top,
}
}
}
#[derive(Component, Reflect, Debug, Clone, PartialEq)]
#[reflect(Component)]
#[require(UIGenID, UIWidgetState, Widget)]
pub struct ColorPicker {
pub entry: usize,
pub red: u8,
pub green: u8,
pub blue: u8,
pub alpha: u8,
pub hue: f32,
pub saturation: f32,
pub value: f32,
}
impl Default for ColorPicker {
fn default() -> Self {
let entry = COLOR_PICKER_ID_POOL.lock().unwrap().acquire();
Self::from_rgba_u8_with_entry(entry, 0x42, 0x85, 0xF4, 255)
}
}
impl ColorPicker {
pub fn from_rgba_u8(red: u8, green: u8, blue: u8, alpha: u8) -> Self {
let entry = COLOR_PICKER_ID_POOL.lock().unwrap().acquire();
Self::from_rgba_u8_with_entry(entry, red, green, blue, alpha)
}
fn from_rgba_u8_with_entry(entry: usize, red: u8, green: u8, blue: u8, alpha: u8) -> Self {
let (hue, saturation, value) = rgb_u8_to_hsv(red, green, blue);
Self {
entry,
red,
green,
blue,
alpha,
hue,
saturation,
value,
}
}
pub fn set_hsv(&mut self, hue: f32, saturation: f32, value: f32) {
self.hue = hue.rem_euclid(360.0);
self.saturation = saturation.clamp(0.0, 1.0);
self.value = value.clamp(0.0, 1.0);
let (r, g, b) = hsv_to_rgb_u8(self.hue, self.saturation, self.value);
self.red = r;
self.green = g;
self.blue = b;
}
pub fn set_rgb(&mut self, red: u8, green: u8, blue: u8) {
self.red = red;
self.green = green;
self.blue = blue;
let (hue, saturation, value) = rgb_u8_to_hsv(red, green, blue);
self.hue = hue;
self.saturation = saturation;
self.value = value;
}
pub fn hex(&self) -> String {
format!("#{:02X}{:02X}{:02X}", self.red, self.green, self.blue)
}
pub fn rgb_string(&self) -> String {
format!("rgb({}, {}, {})", self.red, self.green, self.blue)
}
pub fn rgba_string(&self) -> String {
format!(
"rgba({}, {}, {}, {})",
self.red, self.green, self.blue, self.alpha
)
}
}
pub fn hsv_to_rgb_u8(hue: f32, saturation: f32, value: f32) -> (u8, u8, u8) {
let h = hue.rem_euclid(360.0);
let s = saturation.clamp(0.0, 1.0);
let v = value.clamp(0.0, 1.0);
if s <= f32::EPSILON {
let gray = (v * 255.0).round() as u8;
return (gray, gray, gray);
}
let c = v * s;
let x = c * (1.0 - (((h / 60.0) % 2.0) - 1.0).abs());
let m = v - c;
let (r1, g1, b1) = match h as i32 {
0..=59 => (c, x, 0.0),
60..=119 => (x, c, 0.0),
120..=179 => (0.0, c, x),
180..=239 => (0.0, x, c),
240..=299 => (x, 0.0, c),
_ => (c, 0.0, x),
};
let to_u8 = |f: f32| ((f + m).clamp(0.0, 1.0) * 255.0).round() as u8;
(to_u8(r1), to_u8(g1), to_u8(b1))
}
fn rgb_u8_to_hsv(red: u8, green: u8, blue: u8) -> (f32, f32, f32) {
let r = red as f32 / 255.0;
let g = green as f32 / 255.0;
let b = blue as f32 / 255.0;
let max = r.max(g.max(b));
let min = r.min(g.min(b));
let delta = max - min;
let hue = if delta <= f32::EPSILON {
0.0
} else if (max - r).abs() <= f32::EPSILON {
60.0 * (((g - b) / delta).rem_euclid(6.0))
} else if (max - g).abs() <= f32::EPSILON {
60.0 * (((b - r) / delta) + 2.0)
} else {
60.0 * (((r - g) / delta) + 4.0)
};
let saturation = if max <= f32::EPSILON {
0.0
} else {
delta / max
};
(hue.rem_euclid(360.0), saturation, max)
}
#[derive(Component, Reflect, Debug, Clone, PartialEq, Eq)]
#[reflect(Component)]
#[require(UIGenID, UIWidgetState, Widget)]
pub struct SwitchButton {
pub entry: usize,
pub label: String,
pub icon: Option<String>,
pub selected: bool,
}
impl Default for SwitchButton {
fn default() -> Self {
let entry = SWITCH_BUTTON_ID_POOL.lock().unwrap().acquire();
Self {
entry,
label: String::from(""),
icon: None,
selected: false,
}
}
}
#[derive(Component, Reflect, Debug, Clone, PartialEq)]
#[reflect(Component)]
#[require(UIGenID, UIWidgetState, Widget)]
pub struct ToggleButton {
pub entry: usize,
pub label: String,
#[reflect(ignore)]
pub value: WidgetValue,
pub icon_place: IconPlace,
pub icon_path: Option<String>,
pub selected: bool,
}
impl Default for ToggleButton {
fn default() -> Self {
let entry = TOGGLE_BUTTON_ID_POOL.lock().unwrap().acquire();
Self {
entry,
label: String::from("label"),
value: WidgetValue::new(String::from("")),
icon_path: None,
icon_place: IconPlace::default(),
selected: false,
}
}
}