pub mod scroll;
pub mod button;
pub mod color;
pub mod container;
pub mod fps;
pub mod selection;
pub mod style;
pub mod style_parse;
pub mod text;
pub mod text_input;
pub mod circular;
pub mod dialog;
pub mod image;
pub mod progress_bar;
pub mod checkbox;
pub mod tests;
pub mod base_components;
pub(crate) use scroll::ScrollMovePanelEntity;
pub use base_components::*;
use crate::resources::*;
use crate::reactivity::*;
use crate::utils::*;
use bevy::ecs::system::{EntityCommands, SystemParam};
use bevy::platform::collections::HashMap;
use bevy::ecs::query::QueryData;
use std::cell::RefCell;
use bevy::prelude::*;
pub trait SetupWidget {
fn components(&mut self) -> impl Bundle;
fn build(
&mut self,
reactive_data: &HashMap<String, RVal>,
commands: &mut Commands
) -> Entity;
fn rebuild(
&mut self,
reactive_data: &HashMap<String, RVal>,
old_entity: Entity,
world: &mut World
);
}
#[derive(Clone, Default, PartialEq, Debug)]
pub enum WidgetColor {
#[default]
Default, Dark,
Primary,
PrimaryDark,
Secondary,
Success,
SuccessDark,
Danger,
DangerDark,
Warning,
WarningDark,
Info,
InfoDark,
Transparent,
Custom(String),
CustomSrgba((f32, f32, f32, f32))
}
#[derive(Clone, Copy, Default, PartialEq, Debug)]
pub enum WidgetSize {
#[default]
Default,
Small,
Large,
Custom(f32)
}
#[derive(Default, Clone, Debug)]
pub struct WidgetAttributes {
pub id: Option<String>,
pub class: Option<String>,
pub node: Node,
pub color: WidgetColor,
pub size: WidgetSize,
pub width: Option<String>,
pub height: Option<String>,
pub display: Option<String>,
pub font_handle: Option<Handle<Font>>,
pub image_handle: Option<Handle<Image>>,
pub has_tooltip: bool,
pub tooltip_text: String,
pub model_key: Option<String>,
pub class_split: Vec<String>,
pub border_radius: BorderRadius,
pub(crate) default_visibility: Visibility,
pub(crate) default_z_index: ZIndex,
pub(crate) overrided_background_color: Option<Color>,
pub(crate) overrided_border_color: Option<Color>,
pub(crate) override_text_size: Option<f32>
}
pub trait SetWidgetAttributes: Sized {
fn attributes(&mut self) -> &mut WidgetAttributes;
fn cloned_attrs(&mut self) -> &mut WidgetAttributes;
fn set_model(&mut self, model_key: &str) {
self.attributes().model_key = Some(model_key.to_string());
}
fn set_id(&mut self, id: &str) {
self.attributes().id = Some(id.to_string());
}
fn set_class(&mut self, class: &str) {
self.attributes().class = Some(class.to_string());
self.attributes().class_split = class.split_whitespace().map(|s| s.to_string()).collect();
}
fn set_color(&mut self, color: &str) {
self.attributes().color = WidgetColor::Custom(color.to_string());
}
fn set_size(&mut self, size: f32) {
self.attributes().size = WidgetSize::Custom(size);
}
fn set_width(&mut self, width: &str) {
self.attributes().width = Some(width.to_string());
}
fn set_height(&mut self, height: &str) {
self.attributes().height = Some(height.to_string());
}
fn set_display(&mut self, display: &str) {
self.attributes().display = Some(display.to_string());
}
fn set_tooltip(&mut self, text: &str) {
self.attributes().has_tooltip = true;
self.attributes().tooltip_text = text.to_string();
}
fn _process_built_in_color_class(&mut self) {
if self.cloned_attrs().color != WidgetColor::Default {
return;
}
let mut use_color = WidgetColor::Default;
for class_name in self.cloned_attrs().class_split.iter() {
match class_name.as_str() {
"dark" => use_color = WidgetColor::Dark,
"primary" => use_color = WidgetColor::Primary,
"primary-dark" => use_color = WidgetColor::PrimaryDark,
"secondary" => use_color = WidgetColor::Secondary,
"danger" => use_color = WidgetColor::Danger,
"danger-dark" => use_color = WidgetColor::DangerDark,
"success" => use_color = WidgetColor::Success,
"success-dark" => use_color= WidgetColor::SuccessDark,
"warning" => use_color = WidgetColor::Warning,
"warning-dark" => use_color = WidgetColor::WarningDark,
"info" => use_color = WidgetColor::Info,
"info-dark" => use_color = WidgetColor::InfoDark,
_ => {}
}
}
self.cloned_attrs().color = use_color;
}
fn _process_built_in_alignment_class(&mut self) {
let class_split: Vec<String> = self.cloned_attrs().class_split.clone();
for class_name in class_split.iter() {
match class_name.as_str() {
"jc-start" => self.cloned_attrs().node.justify_content = JustifyContent::Start,
"jc-end" => self.cloned_attrs().node.justify_content = JustifyContent::End,
"jc-flex-start" => self.cloned_attrs().node.justify_content = JustifyContent::FlexStart,
"jc-flex-end" => self.cloned_attrs().node.justify_content = JustifyContent::FlexEnd,
"jc-center" => self.cloned_attrs().node.justify_content = JustifyContent::Center,
"jc-stretch" => self.cloned_attrs().node.justify_content = JustifyContent::Stretch,
"jc-space-between" => self.cloned_attrs().node.justify_content = JustifyContent::SpaceBetween,
"jc-space-evenly" => self.cloned_attrs().node.justify_content = JustifyContent::SpaceEvenly,
"jc-space-around" => self.cloned_attrs().node.justify_content = JustifyContent::SpaceAround,
"ji-start" => self.cloned_attrs().node.justify_items = JustifyItems::Start,
"ji-end" => self.cloned_attrs().node.justify_items = JustifyItems::End,
"ji-center" => self.cloned_attrs().node.justify_items = JustifyItems::Center,
"ji-stretch" => self.cloned_attrs().node.justify_items = JustifyItems::Stretch,
"ji-base-line" => self.cloned_attrs().node.justify_items = JustifyItems::Baseline,
"js-start" => self.cloned_attrs().node.justify_self = JustifySelf::Start,
"js-end" => self.cloned_attrs().node.justify_self = JustifySelf::End,
"js-center" => self.cloned_attrs().node.justify_self = JustifySelf::Center,
"js-stretch" => self.cloned_attrs().node.justify_self = JustifySelf::Stretch,
"js-base-line" => self.cloned_attrs().node.justify_self = JustifySelf::Baseline,
"ac-start" => self.cloned_attrs().node.align_content = AlignContent::Start,
"ac-end" => self.cloned_attrs().node.align_content = AlignContent::End,
"ac-flex-start" => self.cloned_attrs().node.align_content = AlignContent::FlexStart,
"ac-flex-end" => self.cloned_attrs().node.align_content = AlignContent::FlexEnd,
"ac-center" => self.cloned_attrs().node.align_content = AlignContent::Center,
"ac-stretch" => self.cloned_attrs().node.align_content = AlignContent::Stretch,
"ac-space-between" => self.cloned_attrs().node.align_content = AlignContent::SpaceBetween,
"ac-space-evenly" => self.cloned_attrs().node.align_content = AlignContent::SpaceEvenly,
"ac-space-around" => self.cloned_attrs().node.align_content = AlignContent::SpaceAround,
"ai-start" => self.cloned_attrs().node.align_items = AlignItems::Start,
"ai-end" => self.cloned_attrs().node.align_items = AlignItems::End,
"ai-flex-start" => self.cloned_attrs().node.align_items = AlignItems::FlexStart,
"ai-flex-end" => self.cloned_attrs().node.align_items = AlignItems::FlexEnd,
"ai-center" => self.cloned_attrs().node.align_items = AlignItems::Center,
"ai-stretch" => self.cloned_attrs().node.align_items = AlignItems::Stretch,
"ai-base-line" => self.cloned_attrs().node.align_items = AlignItems::Baseline,
"as-start" => self.cloned_attrs().node.align_self = AlignSelf::Start,
"as-end" => self.cloned_attrs().node.align_self = AlignSelf::End,
"as-flex-start" => self.cloned_attrs().node.align_self = AlignSelf::FlexStart,
"as-flex-end" => self.cloned_attrs().node.align_self = AlignSelf::FlexEnd,
"as-center" => self.cloned_attrs().node.align_self = AlignSelf::Center,
"as-stretch" => self.cloned_attrs().node.align_self = AlignSelf::Stretch,
"as-base-line" => self.cloned_attrs().node.align_self = AlignSelf::Baseline,
_ => {}
}
}
}
fn _process_built_in_spacing_class(&mut self) {
let class_split: Vec<String> = self.cloned_attrs().class_split.clone();
let node = &mut self.cloned_attrs().node;
for class_name in class_split.iter() {
if let Some((prefix, value)) = class_name.split_once('-') {
let spacing_value = if value == "auto" {
Val::Auto
} else if let Ok(num) = value.parse::<f32>() {
Val::Px(num * 5.0)
} else {
continue;
};
match prefix {
"mt" => node.margin.top = spacing_value,
"mb" => node.margin.bottom = spacing_value,
"ml" => node.margin.left = spacing_value,
"mr" => node.margin.right = spacing_value,
"my" => {
node.margin.top = spacing_value;
node.margin.bottom = spacing_value;
}
"mx" => {
node.margin.left = spacing_value;
node.margin.right = spacing_value;
}
"pt" => node.padding.top = spacing_value,
"pb" => node.padding.bottom = spacing_value,
"pl" => node.padding.left = spacing_value,
"pr" => node.padding.right = spacing_value,
"py" => {
node.padding.top = spacing_value;
node.padding.bottom = spacing_value;
}
"px" => {
node.padding.left = spacing_value;
node.padding.right = spacing_value;
}
_ => {}
}
}
}
}
fn _process_built_in_border_radius_class(&mut self) {
let class_split: Vec<String> = self.cloned_attrs().class_split.clone();
let br = &mut self.cloned_attrs().border_radius;
*br = BorderRadius::all(Val::Px(5.0));
for class_name in class_split.iter() {
match class_name.as_str() {
"rounded-0" => *br = BorderRadius::all(Val::Px(0.0)),
"rounded-sm" => *br = BorderRadius::all(Val::Px(2.0)),
"rounded-lg" => *br = BorderRadius::all(Val::Px(8.0)),
"rounded-xl" => *br = BorderRadius::all(Val::Px(24.0)),
"rounded-pill" => *br = BorderRadius::all(Val::Px(9999.0)),
"rounded-circle" => *br = BorderRadius::all(Val::Percent(50.0)),
_ => {}
}
}
}
fn _process_built_in_size_class(&mut self) {
if self.cloned_attrs().size != WidgetSize::Default {
return;
}
let mut use_size = WidgetSize::Default;
for class_name in self.cloned_attrs().class_split.iter() {
match class_name.as_str() {
"small" => use_size = WidgetSize::Small,
"large" => use_size = WidgetSize::Large,
_ => {}
}
}
self.cloned_attrs().size = use_size;
}
}
#[derive(Copy, Clone, Debug, PartialEq, Component)]
pub enum WidgetType {
Root, Button,
Container,
Text,
FpsText, TextInput,
Scroll,
Selection,
Circular,
ProgressBar,
Dialog, Image,
BackgroudImage
}
pub struct FamiqBuilder<'a> {
pub asset_server: &'a Res<'a, AssetServer>,
pub ui_root_node: EntityCommands<'a>,
pub resource: Mut<'a, FamiqResource>,
pub reactive_data: Mut<'a, RData>,
}
impl<'a> FamiqBuilder<'a> {
pub fn new(fa_query: &'a mut FaQuery, famiq_resource: &'a mut ResMut<FamiqResource>) -> Self {
Self {
asset_server: &fa_query.asset_server,
ui_root_node: fa_query.commands.entity(famiq_resource.root_node_entity.unwrap()),
resource: famiq_resource.reborrow(),
reactive_data: fa_query.reactive_data.reborrow(),
}
}
pub fn inject(self) {
let boxed = Box::new(self);
let raw = Box::into_raw(boxed); inject_builder(raw as *mut ());
}
pub fn use_font_path(mut self, font_path: &str) -> Self {
self.resource.font_path = font_path.to_string();
self
}
pub fn use_style_path(mut self, style_path: &str) -> Self {
self.resource.style_path = style_path.to_string();
self
}
pub fn hot_reload(mut self) -> Self {
self.resource.hot_reload_styles = true;
self
}
pub fn get_font_handle(&self) -> Handle<Font> {
self.asset_server.load(&self.resource.font_path)
}
pub fn insert_component<T: Bundle>(&mut self, entity: Entity, components: T) {
self.ui_root_node.commands().entity(entity).insert(components);
}
pub fn remove_component<T: Bundle>(&mut self, entity: Entity) {
self.ui_root_node.commands().entity(entity).remove::<T>();
}
pub fn get_entity(&mut self) -> Entity {
self.ui_root_node.id()
}
pub fn clean(&mut self) {
let root_entity = self.get_entity();
self.ui_root_node.commands().entity(root_entity).despawn();
}
}
pub fn hot_reload_is_enabled(famiq_res: Res<FamiqResource>) -> bool {
famiq_res.hot_reload_styles
}
pub fn hot_reload_is_disabled(famiq_res: Res<FamiqResource>) -> bool {
!famiq_res.hot_reload_styles && !famiq_res.external_style_applied
}
pub(crate) fn build_tooltip_node(
attributes: &WidgetAttributes,
commands: &mut Commands,
widget_entity: Entity
) -> Entity {
let txt_font = TextFont {
font: attributes.font_handle.clone().unwrap(),
font_size: get_text_size(&attributes.size),
..default()
};
let tooltip_entity = commands
.spawn((
Node {
position_type: PositionType::Absolute,
top: Val::Px(-28.0),
width: Val::Auto,
height: Val::Auto,
display: Display::None,
max_width: Val::Px(200.),
padding: UiRect {
left: Val::Px(8.0),
right: Val::Px(8.0),
..default()
},
margin: UiRect{
left: Val::Auto,
right: Val::Auto,
..default()
},
..default()
},
GlobalZIndex(4),
BackgroundColor(Color::srgba(1.0, 1.0, 1.0, 0.6)),
BorderRadius::all(Val::Px(5.0)),
Transform::default(),
Text::new(&attributes.tooltip_text),
txt_font,
TextColor(color::BLACK_COLOR),
TextLayout::new_with_justify(JustifyText::Center),
IsFamiqTooltip
))
.id();
commands
.entity(widget_entity)
.add_child(tooltip_entity)
.insert(TooltipEntity(tooltip_entity));
tooltip_entity
}
pub enum WidgetSelector<'a> {
ID(&'a str),
ENTITY(Entity)
}
#[derive(QueryData)]
#[query_data(mutable)]
pub struct StyleQuery {
pub background_color: &'static mut BackgroundColor,
pub border_color: &'static mut BorderColor,
pub border_radius: &'static mut BorderRadius,
pub z_index: &'static mut ZIndex,
pub visibility: &'static mut Visibility,
pub box_shadow: &'static mut BoxShadow,
pub node: &'static mut Node,
pub id: Option<&'static WidgetId>,
pub class: Option<&'static WidgetClasses>,
pub default_style: &'static DefaultWidgetConfig
}
#[derive(QueryData)]
#[query_data(mutable)]
pub struct TextStyleQuery {
pub text_color: &'static mut TextColor,
pub text_font: &'static mut TextFont,
pub id: Option<&'static WidgetId>,
pub class: Option<&'static WidgetClasses>,
pub default_text_style: Option<&'static DefaultTextConfig>,
pub default_text_span_style: Option<&'static DefaultTextSpanConfig>,
}
#[derive(SystemParam)]
pub struct FaStyleQuery<'w, 's> {
pub style_query: Query<'w, 's, StyleQuery>,
pub text_style_query: Query<'w, 's, TextStyleQuery>,
}
impl<'w, 's> FaStyleQuery<'w, 's> {
pub fn get_style_mut(&mut self, selector: WidgetSelector) -> Option<StyleQueryItem<'_>> {
match selector {
WidgetSelector::ID(id) => self
.style_query
.iter_mut()
.find_map(|result| {
result.id
.filter(|w_id| w_id.0 == id)
.map(|_| result)
}),
WidgetSelector::ENTITY(entity) => self.style_query.get_mut(entity).ok(),
}
}
pub fn get_text_style_mut(&mut self, selector: WidgetSelector) -> Option<TextStyleQueryItem<'_>> {
match selector {
WidgetSelector::ID(id) => self
.text_style_query
.iter_mut()
.find_map(|result| {
result.id
.filter(|w_id| w_id.0 == id)
.map(|_| result)
}),
WidgetSelector::ENTITY(entity) => self.text_style_query.get_mut(entity).ok(),
}
}
}
#[derive(QueryData)]
#[query_data(mutable)]
pub struct ContainableQuery {
entity: Entity,
scroll_panel: Option<&'static ScrollMovePanelEntity>,
id: Option<&'static WidgetId>
}
#[derive(SystemParam)]
pub struct FaQuery<'w, 's> {
pub containable_query: Query<'w, 's, ContainableQuery, With<IsFamiqContainableWidget>>,
pub reactive_data: ResMut<'w, RData>,
pub commands: Commands<'w, 's>,
pub asset_server: Res<'w, AssetServer>,
pub reactive_subscriber: ResMut<'w, RSubscriber>,
}
impl<'w, 's> FaQuery<'w, 's> {
pub fn get_containable_item(&self, selector: WidgetSelector) -> Option<ContainableQueryReadOnlyItem<'_>> {
match selector {
WidgetSelector::ID(id) => self
.containable_query
.iter()
.find_map(|result| {
result.id
.filter(|w_id| w_id.0 == id)
.map(|_| result)
}),
WidgetSelector::ENTITY(entity) => self.containable_query.get(entity).ok(),
}
}
pub fn add_children(&mut self, selector: WidgetSelector, children: &[Entity]) {
if let Some(item) = self.get_containable_item(selector) {
if let Some(panel_entity) = item.scroll_panel {
self.commands
.entity(panel_entity.0)
.add_children(children);
return;
}
self.commands.entity(item.entity).add_children(children);
}
}
pub fn insert_children(&mut self, selector: WidgetSelector, index: usize, children: &[Entity]) {
if let Some(item) = self.get_containable_item(selector) {
if let Some(panel_entity) = item.scroll_panel {
self.commands
.entity(panel_entity.0)
.insert_children(index, children);
return;
}
self.commands.entity(item.entity).insert_children(index, children);
}
}
pub fn remove_children(&mut self, children: &[Entity]) {
for child in children {
self.commands.entity(*child).despawn();
}
}
pub fn insert_data(&mut self, key: &str, value: RVal) {
self.reactive_data.data.insert(key.to_string(), value);
}
pub fn insert_str(&mut self, key: &str, value: impl Into<String>) {
self.insert_data(key, RVal::Str(value.into()));
}
pub fn insert_none(&mut self, key: &str) {
self.insert_data(key, RVal::None);
}
pub fn insert_num(&mut self, key: &str, value: i32) {
self.insert_data(key, RVal::Num(value));
}
pub fn insert_bool(&mut self, key: &str, value: bool) {
self.insert_data(key, RVal::Bool(value));
}
pub fn insert_fnum(&mut self, key: &str, value: f32) {
self.insert_data(key, RVal::FNum(value));
}
pub fn insert_str_list(&mut self, key: &str, value: Vec<String>) {
self.insert_data(key, RVal::List(value));
}
pub fn mutate_data(&mut self, key: &str, new_val: RVal) {
let old_val = self.reactive_data.data.get(key);
if old_val.is_none() {
panic!("\n[FamiqError]: mutate_data, key {:?} not found\n", key);
}
if !self.reactive_data.changed_keys.contains(&key.to_string()) {
self.reactive_data.changed_keys.push(key.to_string());
}
self.reactive_data.data.insert(key.to_string(), new_val);
}
pub fn mutate_str(&mut self, key: &str, new_str: &str) {
self.mutate_data(key, RVal::Str(new_str.into()));
}
pub fn mutate_num(&mut self, key: &str, new_num: i32) {
self.mutate_data(key, RVal::Num(new_num));
}
pub fn mutate_fnum(&mut self, key: &str, new_fnum: f32) {
self.mutate_data(key, RVal::FNum(new_fnum));
}
pub fn mutate_bool(&mut self, key: &str, new_bool: bool) {
self.mutate_data(key, RVal::Bool(new_bool));
}
pub fn mutate_none(&mut self, key: &str) {
self.mutate_data(key, RVal::None);
}
pub fn mutate_str_list(&mut self, key: &str, new_list: Vec<String>) {
self.mutate_data(key, RVal::List(new_list));
}
pub fn get_data(&self, key: &str) -> Option<&RVal> {
if let Some(val) = self.reactive_data.data.get(key) {
return Some(val);
}
None
}
pub fn get_data_mut(&mut self, key: &str) -> Option<&mut RVal> {
if self.get_data(key).is_none() {
return None;
}
if !self.reactive_data.changed_keys.contains(&key.to_string()) {
self.reactive_data.changed_keys.push(key.to_string());
}
self.reactive_data.data.get_mut(key)
}
}
#[macro_export]
macro_rules! extract_children {
($vec:ident, children: [ $( $child:expr ),* $(,)? ] $(, $($rest:tt)*)?) => {{
$(
$vec.push($child);
)*
$(
$crate::extract_children!($vec, $($rest)*);
)?
}};
($vec:ident, children: $children_vec:expr $(, $($rest:tt)*)?) => {{
$vec.extend($children_vec);
$(
$crate::extract_children!($vec, $($rest)*);
)?
}};
($vec:ident, $key:ident : $val:expr $(, $($rest:tt)*)?) => {{
$(
$crate::extract_children!($vec, $builder, $($rest)*);
)?
}};
($vec:ident,) => {{}};
}
#[macro_export]
macro_rules! common_attributes {
( $builder:ident, $key:ident : $value:expr ) => {{
match stringify!($key) {
"id" => $builder.set_id($value),
"class" => $builder.set_class($value),
"color" => $builder.set_color($value),
"tooltip" => $builder.set_tooltip($value),
"width" => $builder.set_width($value),
"height" => $builder.set_height($value),
"display" => $builder.set_display($value),
_ => {}
}
}};
}
#[derive(Clone, Debug)]
pub enum BuilderType {
Text(text::TextBuilder),
Button(button::ButtonBuilder),
Checkbox(checkbox::CheckboxBuilder),
Circular(circular::CircularBuilder),
Container(container::ContainerBuilder),
Fps(fps::FpsBuilder),
Image(image::ImageBuilder),
Dialog(dialog::DialogBuilder),
ProgressBar(progress_bar::ProgressBarBuilder),
Selection(selection::SelectionBuilder),
Scroll(scroll::ScrollBuilder)
}
#[derive(Clone, Debug)]
pub struct WidgetBuilder {
pub builder: BuilderType
}
thread_local! {
static GLOBAL_BUILDER: RefCell<Option<*mut ()>> = RefCell::new(None);
}
pub fn inject_builder(ptr: *mut ()) {
GLOBAL_BUILDER.with(|cell| {
*cell.borrow_mut() = Some(ptr);
});
}
pub fn builder_mut<'a>() -> &'a mut FamiqBuilder<'a> {
GLOBAL_BUILDER.with(|cell| {
let ptr = cell
.borrow()
.expect("Can't access global widget builder!") as *mut FamiqBuilder<'a>;
unsafe { &mut *ptr }
})
}