use bevy::ecs::component::Mutable;
use bevy::prelude::*;
use std::collections::{HashMap, HashSet};
use crate::html::{
HtmlAllWidgetsSpawned, HtmlAllWidgetsVisible, HtmlDirty, HtmlEventBindings, HtmlID, HtmlMeta,
HtmlPendingReveal, HtmlStates, HtmlStructureMap, HtmlSystemSet, HtmlWidgetNode, NeedHidden,
ShowWidgetsTimer,
};
use crate::io::CssAsset;
use crate::styles::components::UiStyle;
use crate::styles::{CssClass, CssID, CssSource};
use crate::widgets::body::BodyContentRoot;
use crate::widgets::div::DivContentRoot;
use crate::widgets::{Body, ColorPicker, DatePicker, InputField, UIWidgetState, Widget};
pub struct HtmlBuilderSystem;
#[derive(Component, Clone, Copy, Debug, Eq, PartialEq)]
pub struct HtmlNodeKind {
discriminant: HtmlNodeKindDiscriminant,
}
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
enum HtmlNodeKindDiscriminant {
Body,
Div,
Form,
Dialog,
Divider,
Button,
CheckBox,
ColorPicker,
ChoiceBox,
DatePicker,
FieldSet,
Headline,
HyperLink,
Img,
Input,
Paragraph,
ToolTip,
Badge,
ProgressBar,
RadioButton,
Scrollbar,
Slider,
SwitchButton,
ToggleButton,
ListBox,
}
impl Plugin for HtmlBuilderSystem {
fn build(&self, app: &mut App) {
app.add_message::<HtmlAllWidgetsSpawned>();
app.add_message::<HtmlAllWidgetsVisible>();
app.insert_resource(ShowWidgetsTimer::default());
app.add_systems(Update, build_html_source.in_set(HtmlSystemSet::Build));
app.add_systems(
Update,
show_all_widgets_start
.in_set(HtmlSystemSet::ShowWidgets)
.after(build_html_source),
);
app.add_systems(
Update,
show_all_widgets_finish
.in_set(HtmlSystemSet::ShowWidgets)
.after(show_all_widgets_start),
);
}
}
pub fn build_html_source(
mut commands: Commands,
structure_map: Res<HtmlStructureMap>,
mut html_dirty: ResMut<HtmlDirty>,
mut pending_reveal: Option<ResMut<HtmlPendingReveal>>,
asset_server: Res<AssetServer>,
mut event_writer: MessageWriter<HtmlAllWidgetsSpawned>,
body_query: Query<(Entity, &Body, Option<&HtmlID>)>,
children_query: Query<&Children>,
html_id_query: Query<&HtmlID>,
node_kind_query: Query<&HtmlNodeKind>,
html_id_entity_query: Query<(Entity, &HtmlID)>,
body_content_root_query: Query<&BodyContentRoot>,
div_content_root_query: Query<&DivContentRoot>,
) {
if !html_dirty.0 {
return;
}
let Some(active_list) = structure_map.active.as_ref() else {
html_dirty.0 = false;
html_dirty.1.clear();
return;
};
let rebuild_keys: Vec<String> = if html_dirty.1.is_empty() {
active_list.clone()
} else {
active_list
.iter()
.filter(|active| html_dirty.1.contains(*active))
.cloned()
.collect()
};
html_dirty.0 = false;
for key in &rebuild_keys {
html_dirty.1.remove(key);
}
if rebuild_keys.is_empty() {
return;
}
let html_entity_index = build_html_entity_index(&html_id_entity_query);
for active in &rebuild_keys {
let rebuild_hidden = pending_reveal
.as_mut()
.is_some_and(|pending_reveal| pending_reveal.0.remove(active));
let mut matching_entities = Vec::new();
let mut existing_live_root = None;
for (entity, body, html_id) in body_query.iter() {
if body.html_key.as_deref() != Some(active.as_str()) {
continue;
}
matching_entities.push(entity);
if html_id.is_some() {
existing_live_root = Some(entity);
}
}
if let Some(root) = existing_live_root {
rebuild_structure_children_for_active(
&mut commands,
root,
active,
rebuild_hidden,
&structure_map,
&asset_server,
&children_query,
&html_id_query,
&node_kind_query,
&html_entity_index,
&body_content_root_query,
&div_content_root_query,
);
if rebuild_hidden {
event_writer.write(HtmlAllWidgetsSpawned);
}
continue;
}
for entity in matching_entities {
commands.entity(entity).despawn();
}
spawn_structure_for_active(
&mut commands,
active,
&structure_map,
&asset_server,
&mut event_writer,
);
}
}
fn spawn_structure_for_active(
commands: &mut Commands,
active: &str,
structure_map: &Res<HtmlStructureMap>,
asset_server: &Res<AssetServer>,
event_writer: &mut MessageWriter<HtmlAllWidgetsSpawned>,
) {
if let Some(structure) = structure_map.html_map.get(active) {
for node in structure {
spawn_widget_node(commands, node, asset_server, None, true);
}
event_writer.write(HtmlAllWidgetsSpawned);
} else {
warn!("No structure found for active: {}", active);
}
}
fn rebuild_structure_children_for_active(
commands: &mut Commands,
root: Entity,
active: &str,
start_hidden: bool,
structure_map: &Res<HtmlStructureMap>,
asset_server: &Res<AssetServer>,
children_query: &Query<&Children>,
html_id_query: &Query<&HtmlID>,
node_kind_query: &Query<&HtmlNodeKind>,
html_entity_index: &HashMap<usize, Entity>,
body_content_root_query: &Query<&BodyContentRoot>,
div_content_root_query: &Query<&DivContentRoot>,
) {
let Some(structure) = structure_map.html_map.get(active) else {
warn!("No structure found for active: {}", active);
return;
};
let Some(HtmlWidgetNode::Body(_, _, _, new_children, _, _, _)) = structure.first() else {
warn!(
"No root <body> node found for active '{}' during in-place rebuild",
active
);
return;
};
let content_parent = body_content_root_query
.get(root)
.map(|content| content.0)
.unwrap_or(root);
reconcile_node_children(
commands,
content_parent,
new_children,
start_hidden,
asset_server,
children_query,
html_id_query,
node_kind_query,
html_entity_index,
body_content_root_query,
div_content_root_query,
);
}
fn reconcile_node_children(
commands: &mut Commands,
parent: Entity,
new_nodes: &Vec<HtmlWidgetNode>,
start_hidden: bool,
asset_server: &AssetServer,
children_query: &Query<&Children>,
html_id_query: &Query<&HtmlID>,
node_kind_query: &Query<&HtmlNodeKind>,
html_entity_index: &HashMap<usize, Entity>,
body_content_root_query: &Query<&BodyContentRoot>,
div_content_root_query: &Query<&DivContentRoot>,
) {
let existing_children: Vec<Entity> = children_query
.get(parent)
.map(|children| children.iter().collect())
.unwrap_or_default();
let mut existing_by_id: HashMap<usize, Entity> = HashMap::new();
let mut existing_without_id: Vec<Entity> = Vec::new();
for child in &existing_children {
if let Ok(id) = html_id_query.get(*child) {
existing_by_id.insert(id.0, *child);
} else {
existing_without_id.push(*child);
}
}
let mut final_children: Vec<Entity> =
Vec::with_capacity(new_nodes.len() + existing_without_id.len());
for new_node in new_nodes {
let node_start_hidden = start_hidden;
let node_id = get_node_id(new_node).0;
let relocated_node = is_relocated_widget_node(new_node);
if let Some(existing_entity) = existing_by_id.remove(&node_id) {
if !existing_node_kind_matches(new_node, existing_entity, node_kind_query) {
commands.entity(existing_entity).despawn();
let spawned =
spawn_widget_node(commands, new_node, asset_server, Some(parent), start_hidden);
final_children.push(spawned);
continue;
}
update_existing_widget_node(commands, existing_entity, new_node, node_start_hidden);
if let Some(children) = get_node_children(new_node) {
let nested_parent = resolve_content_parent(
existing_entity,
body_content_root_query,
div_content_root_query,
);
reconcile_node_children(
commands,
nested_parent,
children,
node_start_hidden,
asset_server,
children_query,
html_id_query,
node_kind_query,
html_entity_index,
body_content_root_query,
div_content_root_query,
);
}
final_children.push(existing_entity);
} else if relocated_node {
if let Some(existing_entity) = find_entity_by_html_id(html_entity_index, node_id) {
if !existing_node_kind_matches(new_node, existing_entity, node_kind_query) {
let spawned = spawn_widget_node(
commands,
new_node,
asset_server,
Some(parent),
node_start_hidden,
);
final_children.push(spawned);
continue;
}
update_existing_widget_node(commands, existing_entity, new_node, node_start_hidden);
update_relocated_node_descendants(
commands,
new_node,
node_start_hidden,
asset_server,
children_query,
html_id_query,
node_kind_query,
html_entity_index,
body_content_root_query,
div_content_root_query,
);
continue;
}
let spawned = spawn_widget_node(
commands,
new_node,
asset_server,
Some(parent),
node_start_hidden,
);
final_children.push(spawned);
} else {
let spawned = spawn_widget_node(
commands,
new_node,
asset_server,
Some(parent),
node_start_hidden,
);
final_children.push(spawned);
}
}
for stale in existing_by_id.into_values() {
commands.entity(stale).despawn();
}
final_children.extend(existing_without_id);
if final_children != existing_children {
commands.entity(parent).replace_children(&final_children);
}
}
fn find_entity_by_html_id(html_entity_index: &HashMap<usize, Entity>, id: usize) -> Option<Entity> {
html_entity_index.get(&id).copied()
}
fn build_html_entity_index(
html_id_entity_query: &Query<(Entity, &HtmlID)>,
) -> HashMap<usize, Entity> {
html_id_entity_query
.iter()
.map(|(entity, html_id)| (html_id.0, entity))
.collect()
}
fn update_relocated_node_descendants(
commands: &mut Commands,
node: &HtmlWidgetNode,
start_hidden: bool,
asset_server: &AssetServer,
children_query: &Query<&Children>,
html_id_query: &Query<&HtmlID>,
node_kind_query: &Query<&HtmlNodeKind>,
html_entity_index: &HashMap<usize, Entity>,
body_content_root_query: &Query<&BodyContentRoot>,
div_content_root_query: &Query<&DivContentRoot>,
) {
let Some(children) = get_node_children(node) else {
return;
};
for child in children {
let child_id = get_node_id(child).0;
let Some(existing_child) = find_entity_by_html_id(html_entity_index, child_id) else {
continue;
};
if !existing_node_kind_matches(child, existing_child, node_kind_query) {
continue;
}
update_existing_widget_node(commands, existing_child, child, start_hidden);
if get_node_children(child).is_some() {
let nested_parent = resolve_content_parent(
existing_child,
body_content_root_query,
div_content_root_query,
);
reconcile_node_children(
commands,
nested_parent,
get_node_children(child).unwrap(),
start_hidden,
asset_server,
children_query,
html_id_query,
node_kind_query,
html_entity_index,
body_content_root_query,
div_content_root_query,
);
}
}
}
fn is_bound_date_picker_node(node: &HtmlWidgetNode) -> bool {
matches!(
node,
HtmlWidgetNode::DatePicker(date_picker, _, _, _, _, _)
if date_picker.for_id.is_some()
)
}
fn is_relocated_widget_node(node: &HtmlWidgetNode) -> bool {
if is_bound_date_picker_node(node) {
return true;
}
#[cfg(feature = "extended-dialog")]
if matches!(node, HtmlWidgetNode::Dialog(_, _, _, _, _, _, _)) {
return true;
}
false
}
fn existing_node_kind_matches(
node: &HtmlWidgetNode,
entity: Entity,
node_kind_query: &Query<&HtmlNodeKind>,
) -> bool {
node_kind_query
.get(entity)
.is_ok_and(|existing| *existing == get_node_kind(node))
}
fn resolve_content_parent(
entity: Entity,
body_content_root_query: &Query<&BodyContentRoot>,
div_content_root_query: &Query<&DivContentRoot>,
) -> Entity {
if let Ok(content) = body_content_root_query.get(entity) {
return content.0;
}
if let Ok(content) = div_content_root_query.get(entity) {
return content.0;
}
entity
}
trait HtmlRuntimePreserve {
fn preserve_runtime_fields(&mut self, existing: &Self);
}
macro_rules! preserve_entry {
($($ty:ty),* $(,)?) => {
$(
impl HtmlRuntimePreserve for $ty {
fn preserve_runtime_fields(&mut self, existing: &Self) {
self.entry = existing.entry;
}
}
)*
};
}
preserve_entry!(
Body,
crate::widgets::Form,
crate::widgets::Button,
crate::widgets::CheckBox,
crate::widgets::ChoiceBox,
ColorPicker,
DatePicker,
crate::widgets::Divider,
crate::widgets::FieldSet,
crate::widgets::Headline,
InputField,
crate::widgets::Img,
crate::widgets::HyperLink,
crate::widgets::Paragraph,
crate::widgets::ToolTip,
crate::widgets::Badge,
crate::widgets::ProgressBar,
crate::widgets::RadioButton,
crate::widgets::Slider,
crate::widgets::SwitchButton,
crate::widgets::ToggleButton,
crate::widgets::ListBox,
);
impl HtmlRuntimePreserve for crate::widgets::Div {
fn preserve_runtime_fields(&mut self, existing: &Self) {
self.0 = existing.0;
}
}
impl HtmlRuntimePreserve for crate::widgets::Scrollbar {
fn preserve_runtime_fields(&mut self, existing: &Self) {
self.entry = existing.entry;
self.entity = existing.entity;
self.viewport_extent = existing.viewport_extent;
self.content_extent = existing.content_extent;
}
}
fn get_node_children(node: &HtmlWidgetNode) -> Option<&Vec<HtmlWidgetNode>> {
if let HtmlWidgetNode::Body(_, _, _, children, _, _, _) = node {
return Some(children);
}
if let HtmlWidgetNode::Div(_, _, _, children, _, _, _) = node {
return Some(children);
}
if let HtmlWidgetNode::Form(_, _, _, children, _, _, _) = node {
return Some(children);
}
if let HtmlWidgetNode::FieldSet(_, _, _, children, _, _, _) = node {
return Some(children);
}
#[cfg(feature = "extended-dialog")]
if let HtmlWidgetNode::Dialog(_, _, _, children, _, _, _) = node {
return Some(children);
}
None
}
fn update_existing_widget_node(
commands: &mut Commands,
entity: Entity,
node: &HtmlWidgetNode,
start_hidden: bool,
) {
sync_meta_component(commands, entity, get_node_kind(node));
match node {
HtmlWidgetNode::Body(body, meta, states, _, functions, widget, id) => {
update_with_meta(
commands,
entity,
body.clone(),
meta,
states,
functions,
widget,
id,
start_hidden,
);
}
HtmlWidgetNode::Button(button, meta, states, functions, widget, id) => {
update_with_meta(
commands,
entity,
button.clone(),
meta,
states,
functions,
widget,
id,
start_hidden,
);
}
HtmlWidgetNode::CheckBox(checkbox, meta, states, functions, widget, id) => {
update_with_meta(
commands,
entity,
checkbox.clone(),
meta,
states,
functions,
widget,
id,
start_hidden,
);
}
HtmlWidgetNode::ColorPicker(color_picker, meta, states, functions, widget, id) => {
update_color_picker_with_meta(
commands,
entity,
color_picker.clone(),
meta,
states,
functions,
widget,
id,
start_hidden,
);
}
HtmlWidgetNode::ChoiceBox(choice_box, meta, states, functions, widget, id) => {
update_with_meta(
commands,
entity,
choice_box.clone(),
meta,
states,
functions,
widget,
id,
start_hidden,
);
}
HtmlWidgetNode::DatePicker(date_picker, meta, states, functions, widget, id) => {
update_date_picker_with_meta(
commands,
entity,
date_picker.clone(),
meta,
states,
functions,
widget,
id,
start_hidden,
);
}
HtmlWidgetNode::Div(div, meta, states, _, functions, widget, id) => {
update_with_meta(
commands,
entity,
div.clone(),
meta,
states,
functions,
widget,
id,
start_hidden,
);
}
HtmlWidgetNode::Form(form, meta, states, _, functions, widget, id) => {
update_with_meta(
commands,
entity,
form.clone(),
meta,
states,
functions,
widget,
id,
start_hidden,
);
}
#[cfg(feature = "extended-dialog")]
HtmlWidgetNode::Dialog(dialog, meta, states, _, functions, widget, id) => {
update_dialog_with_meta(
commands,
entity,
dialog.clone(),
meta,
states,
functions,
widget,
id,
start_hidden,
);
}
HtmlWidgetNode::Divider(divider, meta, states, functions, widget, id) => {
update_with_meta(
commands,
entity,
divider.clone(),
meta,
states,
functions,
widget,
id,
start_hidden,
);
}
HtmlWidgetNode::FieldSet(fieldset, meta, states, _, functions, widget, id) => {
update_with_meta(
commands,
entity,
fieldset.clone(),
meta,
states,
functions,
widget,
id,
start_hidden,
);
}
HtmlWidgetNode::Headline(headline, meta, states, functions, widget, id) => {
update_with_meta(
commands,
entity,
headline.clone(),
meta,
states,
functions,
widget,
id,
start_hidden,
);
}
HtmlWidgetNode::HyperLink(hyper_link, meta, states, functions, widget, id) => {
update_with_meta(
commands,
entity,
hyper_link.clone(),
meta,
states,
functions,
widget,
id,
start_hidden,
);
}
HtmlWidgetNode::Img(img, meta, states, functions, widget, id) => {
update_with_meta(
commands,
entity,
img.clone(),
meta,
states,
functions,
widget,
id,
start_hidden,
);
}
HtmlWidgetNode::Input(input, meta, states, functions, widget, id) => {
update_input_with_meta(
commands,
entity,
input.clone(),
meta,
states,
functions,
widget,
id,
start_hidden,
);
}
HtmlWidgetNode::Paragraph(paragraph, meta, states, functions, widget, id) => {
update_with_meta(
commands,
entity,
paragraph.clone(),
meta,
states,
functions,
widget,
id,
start_hidden,
);
}
HtmlWidgetNode::ToolTip(tooltip, meta, states, functions, widget, id) => {
update_with_meta(
commands,
entity,
tooltip.clone(),
meta,
states,
functions,
widget,
id,
start_hidden,
);
}
HtmlWidgetNode::Badge(badge, meta, states, functions, widget, id) => {
update_with_meta(
commands,
entity,
badge.clone(),
meta,
states,
functions,
widget,
id,
start_hidden,
);
}
HtmlWidgetNode::ProgressBar(progress_bar, meta, states, functions, widget, id) => {
update_with_meta(
commands,
entity,
progress_bar.clone(),
meta,
states,
functions,
widget,
id,
start_hidden,
);
}
HtmlWidgetNode::RadioButton(radio_button, meta, states, functions, widget, id) => {
update_with_meta(
commands,
entity,
radio_button.clone(),
meta,
states,
functions,
widget,
id,
start_hidden,
);
}
HtmlWidgetNode::Scrollbar(scroll_bar, meta, states, functions, widget, id) => {
update_with_meta(
commands,
entity,
scroll_bar.clone(),
meta,
states,
functions,
widget,
id,
start_hidden,
);
}
HtmlWidgetNode::Slider(slider, meta, states, functions, widget, id) => {
update_with_meta(
commands,
entity,
slider.clone(),
meta,
states,
functions,
widget,
id,
start_hidden,
);
}
HtmlWidgetNode::SwitchButton(switch_button, meta, states, functions, widget, id) => {
update_with_meta(
commands,
entity,
switch_button.clone(),
meta,
states,
functions,
widget,
id,
start_hidden,
);
}
HtmlWidgetNode::ToggleButton(toggle_button, meta, states, functions, widget, id) => {
update_with_meta(
commands,
entity,
toggle_button.clone(),
meta,
states,
functions,
widget,
id,
start_hidden,
);
}
HtmlWidgetNode::ListBox(list_box, meta, states, functions, widget, id) => {
update_with_meta(
commands,
entity,
list_box.clone(),
meta,
states,
functions,
widget,
id,
start_hidden,
);
}
}
}
fn update_with_meta<T: Component<Mutability = Mutable> + PartialEq + HtmlRuntimePreserve>(
commands: &mut Commands,
entity: Entity,
component: T,
meta: &HtmlMeta,
states: &HtmlStates,
functions: &HtmlEventBindings,
widget: &Widget,
id: &HtmlID,
start_hidden: bool,
) {
update_meta_components(
commands,
entity,
meta,
states,
functions,
widget,
id,
start_hidden,
);
sync_component(commands, entity, component);
}
fn sync_component<T>(commands: &mut Commands, entity: Entity, component: T)
where
T: Component<Mutability = Mutable> + PartialEq + HtmlRuntimePreserve,
{
commands.queue(move |world: &mut World| {
let mut next = component;
if let Some(mut existing) = world.get_mut::<T>(entity) {
next.preserve_runtime_fields(&existing);
if *existing != next {
*existing = next;
}
} else if let Ok(mut entity_mut) = world.get_entity_mut(entity) {
entity_mut.insert(next);
}
});
}
fn sync_meta_component<T>(commands: &mut Commands, entity: Entity, component: T)
where
T: Component<Mutability = Mutable> + PartialEq,
{
commands.queue(move |world: &mut World| {
if let Some(mut existing) = world.get_mut::<T>(entity) {
if *existing != component {
*existing = component;
}
} else if let Ok(mut entity_mut) = world.get_entity_mut(entity) {
entity_mut.insert(component);
}
});
}
fn update_widget_state_from_html(commands: &mut Commands, entity: Entity, states: &HtmlStates) {
let readonly = states.readonly;
let disabled = states.disabled;
commands.queue(move |world: &mut World| {
let Some(mut state) = world.get_mut::<UIWidgetState>(entity) else {
return;
};
if state.readonly != readonly {
state.readonly = readonly;
}
if state.disabled != disabled {
state.disabled = disabled;
}
});
}
fn update_meta_components(
commands: &mut Commands,
entity: Entity,
meta: &HtmlMeta,
states: &HtmlStates,
functions: &HtmlEventBindings,
widget: &Widget,
id: &HtmlID,
start_hidden: bool,
) {
sync_meta_component(commands, entity, functions.clone());
sync_meta_component(commands, entity, widget.clone());
sync_meta_component(commands, entity, id.clone());
sync_meta_component(commands, entity, meta.inner_content.clone());
sync_meta_component(commands, entity, CssSource(meta.css.clone()));
sync_meta_component(
commands,
entity,
CssClass(meta.class.clone().unwrap_or_default()),
);
sync_meta_component(commands, entity, CssID(meta.id.clone().unwrap_or_default()));
sync_meta_component(
commands,
entity,
if start_hidden || states.hidden {
Visibility::Hidden
} else {
Visibility::Inherited
},
);
update_widget_state_from_html(commands, entity, states);
if let Some(inline_style) = &meta.style {
sync_meta_component(commands, entity, inline_style.clone());
} else {
commands.entity(entity).remove::<crate::html::HtmlStyle>();
}
if let Some(text_binding) = &meta.text_binding {
sync_meta_component(commands, entity, text_binding.clone());
} else {
commands
.entity(entity)
.remove::<crate::html::HtmlTextBinding>();
}
if let Some(validation) = &meta.validation {
sync_meta_component(commands, entity, validation.clone());
} else {
commands
.entity(entity)
.remove::<crate::widgets::ValidationRules>();
}
if states.hidden {
commands.entity(entity).insert(NeedHidden);
} else {
commands.entity(entity).remove::<NeedHidden>();
}
}
fn update_input_with_meta(
commands: &mut Commands,
entity: Entity,
component: InputField,
meta: &HtmlMeta,
states: &HtmlStates,
functions: &HtmlEventBindings,
widget: &Widget,
id: &HtmlID,
start_hidden: bool,
) {
update_meta_components(
commands,
entity,
meta,
states,
functions,
widget,
id,
start_hidden,
);
commands.queue(move |world: &mut World| {
let mut next = component;
if let Some(mut existing) = world.get_mut::<InputField>(entity) {
next.entry = existing.entry;
next.cursor_position = existing.cursor_position.min(next.text.len());
if *existing != next {
*existing = next;
}
} else if let Ok(mut entity_mut) = world.get_entity_mut(entity) {
entity_mut.insert(next);
}
});
}
fn update_date_picker_with_meta(
commands: &mut Commands,
entity: Entity,
component: DatePicker,
meta: &HtmlMeta,
states: &HtmlStates,
functions: &HtmlEventBindings,
widget: &Widget,
id: &HtmlID,
start_hidden: bool,
) {
update_meta_components(
commands,
entity,
meta,
states,
functions,
widget,
id,
start_hidden,
);
commands.queue(move |world: &mut World| {
let mut next = component;
if let Some(mut existing) = world.get_mut::<DatePicker>(entity) {
next.entry = existing.entry;
if *existing != next {
*existing = next;
}
} else if let Ok(mut entity_mut) = world.get_entity_mut(entity) {
entity_mut.insert(next);
}
});
}
#[cfg(feature = "extended-dialog")]
fn update_dialog_with_meta(
commands: &mut Commands,
entity: Entity,
component: crate::dialog::DialogWidget,
meta: &HtmlMeta,
states: &HtmlStates,
functions: &HtmlEventBindings,
widget: &Widget,
id: &HtmlID,
_start_hidden: bool,
) {
update_meta_components(commands, entity, meta, states, functions, widget, id, true);
crate::dialog::apply_dialog_widget_overlay_components(commands, entity, meta.style.as_ref());
commands.entity(entity).insert(NeedHidden);
commands.queue(move |world: &mut World| {
let mut next = component;
if let Some(mut existing) = world.get_mut::<crate::dialog::DialogWidget>(entity) {
next.open = existing.open;
if *existing != next {
*existing = next;
}
} else if let Ok(mut entity_mut) = world.get_entity_mut(entity) {
entity_mut.insert(next);
}
let runtime_components = world
.get::<crate::dialog::DialogWidget>(entity)
.map(crate::dialog::dialog_widget_runtime_components)
.unwrap_or((Visibility::Hidden, Pickable::IGNORE));
if let Ok(mut entity_mut) = world.get_entity_mut(entity) {
entity_mut.insert(runtime_components);
}
});
}
fn update_color_picker_with_meta(
commands: &mut Commands,
entity: Entity,
component: ColorPicker,
meta: &HtmlMeta,
states: &HtmlStates,
functions: &HtmlEventBindings,
widget: &Widget,
id: &HtmlID,
start_hidden: bool,
) {
update_meta_components(
commands,
entity,
meta,
states,
functions,
widget,
id,
start_hidden,
);
commands.queue(move |world: &mut World| {
let mut next = component;
if let Some(mut existing) = world.get_mut::<ColorPicker>(entity) {
next.entry = existing.entry;
if *existing != next {
*existing = next;
}
} else if let Ok(mut entity_mut) = world.get_entity_mut(entity) {
entity_mut.insert(next);
}
});
}
fn get_node_id(node: &HtmlWidgetNode) -> &HtmlID {
match node {
HtmlWidgetNode::Body(_, _, _, _, _, _, id)
| HtmlWidgetNode::Button(_, _, _, _, _, id)
| HtmlWidgetNode::CheckBox(_, _, _, _, _, id)
| HtmlWidgetNode::ColorPicker(_, _, _, _, _, id)
| HtmlWidgetNode::ChoiceBox(_, _, _, _, _, id)
| HtmlWidgetNode::DatePicker(_, _, _, _, _, id)
| HtmlWidgetNode::Divider(_, _, _, _, _, id)
| HtmlWidgetNode::Headline(_, _, _, _, _, id)
| HtmlWidgetNode::HyperLink(_, _, _, _, _, id)
| HtmlWidgetNode::Img(_, _, _, _, _, id)
| HtmlWidgetNode::Input(_, _, _, _, _, id)
| HtmlWidgetNode::Paragraph(_, _, _, _, _, id)
| HtmlWidgetNode::ToolTip(_, _, _, _, _, id)
| HtmlWidgetNode::Badge(_, _, _, _, _, id)
| HtmlWidgetNode::ProgressBar(_, _, _, _, _, id)
| HtmlWidgetNode::RadioButton(_, _, _, _, _, id)
| HtmlWidgetNode::Scrollbar(_, _, _, _, _, id)
| HtmlWidgetNode::Slider(_, _, _, _, _, id)
| HtmlWidgetNode::SwitchButton(_, _, _, _, _, id)
| HtmlWidgetNode::ToggleButton(_, _, _, _, _, id)
| HtmlWidgetNode::ListBox(_, _, _, _, _, id)
| HtmlWidgetNode::Div(_, _, _, _, _, _, id)
| HtmlWidgetNode::Form(_, _, _, _, _, _, id)
| HtmlWidgetNode::FieldSet(_, _, _, _, _, _, id) => id,
#[cfg(feature = "extended-dialog")]
HtmlWidgetNode::Dialog(_, _, _, _, _, _, id) => id,
}
}
fn get_node_kind(node: &HtmlWidgetNode) -> HtmlNodeKind {
let discriminant = match node {
HtmlWidgetNode::Body(..) => HtmlNodeKindDiscriminant::Body,
HtmlWidgetNode::Div(..) => HtmlNodeKindDiscriminant::Div,
HtmlWidgetNode::Form(..) => HtmlNodeKindDiscriminant::Form,
#[cfg(feature = "extended-dialog")]
HtmlWidgetNode::Dialog(..) => HtmlNodeKindDiscriminant::Dialog,
HtmlWidgetNode::Divider(..) => HtmlNodeKindDiscriminant::Divider,
HtmlWidgetNode::Button(..) => HtmlNodeKindDiscriminant::Button,
HtmlWidgetNode::CheckBox(..) => HtmlNodeKindDiscriminant::CheckBox,
HtmlWidgetNode::ColorPicker(..) => HtmlNodeKindDiscriminant::ColorPicker,
HtmlWidgetNode::ChoiceBox(..) => HtmlNodeKindDiscriminant::ChoiceBox,
HtmlWidgetNode::DatePicker(..) => HtmlNodeKindDiscriminant::DatePicker,
HtmlWidgetNode::FieldSet(..) => HtmlNodeKindDiscriminant::FieldSet,
HtmlWidgetNode::Headline(..) => HtmlNodeKindDiscriminant::Headline,
HtmlWidgetNode::HyperLink(..) => HtmlNodeKindDiscriminant::HyperLink,
HtmlWidgetNode::Img(..) => HtmlNodeKindDiscriminant::Img,
HtmlWidgetNode::Input(..) => HtmlNodeKindDiscriminant::Input,
HtmlWidgetNode::Paragraph(..) => HtmlNodeKindDiscriminant::Paragraph,
HtmlWidgetNode::ToolTip(..) => HtmlNodeKindDiscriminant::ToolTip,
HtmlWidgetNode::Badge(..) => HtmlNodeKindDiscriminant::Badge,
HtmlWidgetNode::ProgressBar(..) => HtmlNodeKindDiscriminant::ProgressBar,
HtmlWidgetNode::RadioButton(..) => HtmlNodeKindDiscriminant::RadioButton,
HtmlWidgetNode::Scrollbar(..) => HtmlNodeKindDiscriminant::Scrollbar,
HtmlWidgetNode::Slider(..) => HtmlNodeKindDiscriminant::Slider,
HtmlWidgetNode::SwitchButton(..) => HtmlNodeKindDiscriminant::SwitchButton,
HtmlWidgetNode::ToggleButton(..) => HtmlNodeKindDiscriminant::ToggleButton,
HtmlWidgetNode::ListBox(..) => HtmlNodeKindDiscriminant::ListBox,
};
HtmlNodeKind { discriminant }
}
fn show_all_widgets_start(
mut events: MessageReader<HtmlAllWidgetsSpawned>,
mut timer: ResMut<ShowWidgetsTimer>,
) {
for _event in events.read() {
timer.timer = Timer::from_seconds(0.0, TimerMode::Once);
timer.active = true;
debug!("Starting reveal timer before showing widgets");
}
}
fn show_all_widgets_finish(
time: Res<Time>,
mut timer: ResMut<ShowWidgetsTimer>,
mut query: Query<(&mut Visibility, &HtmlID), (With<Widget>, Without<NeedHidden>)>,
style_query: Query<
(&HtmlID, Option<&CssSource>, Option<&UiStyle>),
(With<Widget>, Without<NeedHidden>),
>,
css_assets: Option<Res<Assets<CssAsset>>>,
current_body: Query<&Body>,
structure_map: Res<HtmlStructureMap>,
mut event_writer: MessageWriter<HtmlAllWidgetsVisible>,
) {
if timer.active && timer.timer.tick(time.delta()).is_finished() {
let Some(active_list) = structure_map.active.as_ref() else {
return;
};
let mut valid_ids = Vec::new();
for active in active_list {
if let Some(map_nodes) = structure_map.html_map.get(active.as_str()) {
collect_html_ids(map_nodes, &mut valid_ids);
}
}
if valid_ids.is_empty() {
return;
}
let valid_id_set = valid_ids.iter().map(|id| id.0).collect::<HashSet<_>>();
if let Some(css_assets) = css_assets.as_deref() {
if !widgets_ready_for_reveal(&valid_id_set, &style_query, css_assets) {
return;
}
}
for body in current_body.iter() {
if let Some(bind) = body.html_key.as_ref() {
if active_list.iter().any(|active| active == bind) {
for (mut visibility, widget_id) in query.iter_mut() {
if valid_id_set.contains(&widget_id.0) {
*visibility = Visibility::Inherited;
}
}
timer.active = false;
event_writer.write(HtmlAllWidgetsVisible);
debug!(
"All widgets for '{:?}' are now visible after 100ms delay",
active_list
);
break;
}
}
}
}
}
fn widgets_ready_for_reveal(
valid_ids: &HashSet<usize>,
style_query: &Query<
(&HtmlID, Option<&CssSource>, Option<&UiStyle>),
(With<Widget>, Without<NeedHidden>),
>,
css_assets: &Assets<CssAsset>,
) -> bool {
let mut checked_any = false;
for (html_id, css_source, ui_style) in style_query.iter() {
if !valid_ids.contains(&html_id.0) {
continue;
}
checked_any = true;
if let Some(css_source) = css_source {
let all_css_loaded = css_source
.0
.iter()
.all(|handle| css_assets.get(handle).is_some());
if !all_css_loaded {
return false;
}
}
let Some(ui_style) = ui_style else {
return false;
};
if ui_style.active_style.is_none() {
return false;
}
}
checked_any
}
fn collect_html_ids(nodes: &Vec<HtmlWidgetNode>, ids: &mut Vec<HtmlID>) {
for node in nodes {
match node {
HtmlWidgetNode::Body(_, _, _, children, _, _, id) => {
ids.push(id.clone());
collect_html_ids(children, ids);
}
HtmlWidgetNode::Button(_, _, _, _, _, id)
| HtmlWidgetNode::CheckBox(_, _, _, _, _, id)
| HtmlWidgetNode::ColorPicker(_, _, _, _, _, id)
| HtmlWidgetNode::ChoiceBox(_, _, _, _, _, id)
| HtmlWidgetNode::DatePicker(_, _, _, _, _, id)
| HtmlWidgetNode::Divider(_, _, _, _, _, id)
| HtmlWidgetNode::Headline(_, _, _, _, _, id)
| HtmlWidgetNode::HyperLink(_, _, _, _, _, id)
| HtmlWidgetNode::Img(_, _, _, _, _, id)
| HtmlWidgetNode::Input(_, _, _, _, _, id)
| HtmlWidgetNode::Paragraph(_, _, _, _, _, id)
| HtmlWidgetNode::ToolTip(_, _, _, _, _, id)
| HtmlWidgetNode::Badge(_, _, _, _, _, id)
| HtmlWidgetNode::ProgressBar(_, _, _, _, _, id)
| HtmlWidgetNode::RadioButton(_, _, _, _, _, id)
| HtmlWidgetNode::Scrollbar(_, _, _, _, _, id)
| HtmlWidgetNode::Slider(_, _, _, _, _, id)
| HtmlWidgetNode::SwitchButton(_, _, _, _, _, id)
| HtmlWidgetNode::ToggleButton(_, _, _, _, _, id)
| HtmlWidgetNode::ListBox(_, _, _, _, _, id) => {
ids.push(id.clone());
}
HtmlWidgetNode::Div(_, _, _, children, _, _, id) => {
ids.push(id.clone());
collect_html_ids(children, ids);
}
#[cfg(feature = "extended-dialog")]
HtmlWidgetNode::Dialog(_, _, _, children, _, _, id) => {
ids.push(id.clone());
collect_html_ids(children, ids);
}
HtmlWidgetNode::Form(_, _, _, children, _, _, id) => {
ids.push(id.clone());
collect_html_ids(children, ids);
}
HtmlWidgetNode::FieldSet(_, _, _, children, _, _, id) => {
ids.push(id.clone());
collect_html_ids(children, ids);
}
}
}
}
fn spawn_widget_node(
commands: &mut Commands,
node: &HtmlWidgetNode,
asset_server: &AssetServer,
parent: Option<Entity>,
start_hidden: bool,
) -> Entity {
let entity = match node {
HtmlWidgetNode::Body(body, meta, states, children, functions, widget, id) => {
let entity = spawn_with_meta(
commands,
body.clone(),
meta,
states,
functions,
widget,
id,
start_hidden,
);
for child in children {
let child_start_hidden = start_hidden;
spawn_widget_node(
commands,
child,
asset_server,
Some(entity),
child_start_hidden,
);
}
entity
}
HtmlWidgetNode::Button(button, meta, states, functions, widget, id) => spawn_with_meta(
commands,
button.clone(),
meta,
states,
functions,
widget,
id,
start_hidden,
),
HtmlWidgetNode::CheckBox(checkbox, meta, states, functions, widget, id) => spawn_with_meta(
commands,
checkbox.clone(),
meta,
states,
functions,
widget,
id,
start_hidden,
),
HtmlWidgetNode::ColorPicker(color_picker, meta, states, functions, widget, id) => {
spawn_with_meta(
commands,
color_picker.clone(),
meta,
states,
functions,
widget,
id,
start_hidden,
)
}
HtmlWidgetNode::ChoiceBox(choice_box, meta, states, functions, widget, id) => {
spawn_with_meta(
commands,
choice_box.clone(),
meta,
states,
functions,
widget,
id,
start_hidden,
)
}
HtmlWidgetNode::DatePicker(date_picker, meta, states, functions, widget, id) => {
spawn_with_meta(
commands,
date_picker.clone(),
meta,
states,
functions,
widget,
id,
start_hidden,
)
}
HtmlWidgetNode::Div(div, meta, states, children, functions, widget, id) => {
let entity = spawn_with_meta(
commands,
div.clone(),
meta,
states,
functions,
widget,
id,
start_hidden,
);
for child in children {
let child_start_hidden = start_hidden;
spawn_widget_node(
commands,
child,
asset_server,
Some(entity),
child_start_hidden,
);
}
entity
}
HtmlWidgetNode::Form(form, meta, states, children, functions, widget, id) => {
let entity = spawn_with_meta(
commands,
form.clone(),
meta,
states,
functions,
widget,
id,
start_hidden,
);
for child in children {
let child_start_hidden = start_hidden;
spawn_widget_node(
commands,
child,
asset_server,
Some(entity),
child_start_hidden,
);
}
entity
}
#[cfg(feature = "extended-dialog")]
HtmlWidgetNode::Dialog(dialog, meta, states, children, functions, widget, id) => {
let entity = spawn_with_meta(
commands,
dialog.clone(),
meta,
states,
functions,
widget,
id,
true,
);
commands.entity(entity).insert(NeedHidden);
for child in children {
let child_start_hidden = start_hidden;
spawn_widget_node(
commands,
child,
asset_server,
Some(entity),
child_start_hidden,
);
}
entity
}
HtmlWidgetNode::Divider(divider, meta, states, functions, widget, id) => spawn_with_meta(
commands,
divider.clone(),
meta,
states,
functions,
widget,
id,
start_hidden,
),
HtmlWidgetNode::FieldSet(fieldset, meta, states, children, functions, widget, id) => {
let entity = spawn_with_meta(
commands,
fieldset.clone(),
meta,
states,
functions,
widget,
id,
start_hidden,
);
for child in children {
let child_start_hidden = start_hidden;
spawn_widget_node(
commands,
child,
asset_server,
Some(entity),
child_start_hidden,
);
}
entity
}
HtmlWidgetNode::Headline(headline, meta, states, functions, widget, id) => spawn_with_meta(
commands,
headline.clone(),
meta,
states,
functions,
widget,
id,
start_hidden,
),
HtmlWidgetNode::HyperLink(hyper_link, meta, states, functions, widget, id) => {
spawn_with_meta(
commands,
hyper_link.clone(),
meta,
states,
functions,
widget,
id,
start_hidden,
)
}
HtmlWidgetNode::Img(img, meta, states, functions, widget, id) => spawn_with_meta(
commands,
img.clone(),
meta,
states,
functions,
widget,
id,
start_hidden,
),
HtmlWidgetNode::Input(input, meta, states, functions, widget, id) => spawn_with_meta(
commands,
input.clone(),
meta,
states,
functions,
widget,
id,
start_hidden,
),
HtmlWidgetNode::Paragraph(paragraph, meta, states, functions, widget, id) => {
spawn_with_meta(
commands,
paragraph.clone(),
meta,
states,
functions,
widget,
id,
start_hidden,
)
}
HtmlWidgetNode::ToolTip(tooltip, meta, states, functions, widget, id) => spawn_with_meta(
commands,
tooltip.clone(),
meta,
states,
functions,
widget,
id,
start_hidden,
),
HtmlWidgetNode::Badge(badge, meta, states, functions, widget, id) => spawn_with_meta(
commands,
badge.clone(),
meta,
states,
functions,
widget,
id,
start_hidden,
),
HtmlWidgetNode::ProgressBar(progress_bar, meta, states, functions, widget, id) => {
spawn_with_meta(
commands,
progress_bar.clone(),
meta,
states,
functions,
widget,
id,
start_hidden,
)
}
HtmlWidgetNode::RadioButton(radio_button, meta, states, functions, widget, id) => {
spawn_with_meta(
commands,
radio_button.clone(),
meta,
states,
functions,
widget,
id,
start_hidden,
)
}
HtmlWidgetNode::Scrollbar(scroll_bar, meta, states, functions, widget, id) => {
spawn_with_meta(
commands,
scroll_bar.clone(),
meta,
states,
functions,
widget,
id,
start_hidden,
)
}
HtmlWidgetNode::Slider(slider, meta, states, functions, widget, id) => spawn_with_meta(
commands,
slider.clone(),
meta,
states,
functions,
widget,
id,
start_hidden,
),
HtmlWidgetNode::SwitchButton(switch_button, meta, states, functions, widget, id) => {
spawn_with_meta(
commands,
switch_button.clone(),
meta,
states,
functions,
widget,
id,
start_hidden,
)
}
HtmlWidgetNode::ToggleButton(toggle_button, meta, states, functions, widget, id) => {
spawn_with_meta(
commands,
toggle_button.clone(),
meta,
states,
functions,
widget,
id,
start_hidden,
)
}
HtmlWidgetNode::ListBox(list_box, meta, states, functions, widget, id) => spawn_with_meta(
commands,
list_box.clone(),
meta,
states,
functions,
widget,
id,
start_hidden,
),
};
if let Some(parent) = parent {
commands.entity(parent).add_child(entity);
}
commands.entity(entity).insert(get_node_kind(node));
entity
}
fn spawn_with_meta<T: Component>(
commands: &mut Commands,
component: T,
meta: &HtmlMeta,
states: &HtmlStates,
functions: &HtmlEventBindings,
widget: &Widget,
id: &HtmlID,
start_hidden: bool,
) -> Entity {
let mut ui_state = UIWidgetState::default();
ui_state.readonly = states.readonly;
ui_state.disabled = states.disabled;
let entity = commands
.spawn((
component,
functions.clone(),
widget.clone(),
id.clone(),
meta.inner_content.clone(),
Node::default(),
CssSource(meta.css.clone()),
CssClass(meta.class.clone().unwrap_or_default()),
CssID(meta.id.clone().unwrap_or_default()),
ui_state,
if start_hidden || states.hidden {
Visibility::Hidden
} else {
Visibility::Inherited
},
))
.id();
if let Some(inline_style) = &meta.style {
commands.entity(entity).insert(inline_style.clone());
}
if let Some(text_binding) = &meta.text_binding {
commands.entity(entity).insert(text_binding.clone());
}
if let Some(validation) = &meta.validation {
commands.entity(entity).insert(validation.clone());
}
if states.hidden {
commands.entity(entity).insert(NeedHidden);
}
entity
}