diff --git a/Cargo.toml b/Cargo.toml index 55061642..a06603b1 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -56,6 +56,12 @@ bevy_scroll_box = { path = "bevy_widgets/bevy_scroll_box" } bevy_footer_bar = { path = "bevy_widgets/bevy_footer_bar" } bevy_toolbar = { path = "bevy_widgets/bevy_toolbar" } bevy_tooltips = { path = "bevy_widgets/bevy_tooltips" } +bevy_text_editing = { path = "bevy_widgets/bevy_text_editing" } +bevy_field_forms = { path = "bevy_widgets/bevy_field_forms" } +bevy_focus = { path = "bevy_widgets/bevy_focus" } +bevy_incomplete_bsn = { path = "bevy_widgets/bevy_incomplete_bsn" } +bevy_collapsing_header = { path = "bevy_widgets/bevy_collapsing_header" } +bevy_entity_inspector = { path = "bevy_widgets/bevy_entity_inspector" } # general crates bevy_editor_core = { path = "crates/bevy_editor_core" } @@ -71,3 +77,4 @@ bevy_transform_gizmos = { path = "crates/bevy_transform_gizmos" } bevy_undo = { path = "crates/bevy_undo" } bevy_infinite_grid = { path = "crates/bevy_infinite_grid" } bevy_editor_cam = { path = "crates/bevy_editor_cam" } +bevy_clipboard = { path = "crates/bevy_clipboard" } diff --git a/bevy_editor_panes/bevy_properties_pane/Cargo.toml b/bevy_editor_panes/bevy_properties_pane/Cargo.toml index 39f86caf..b2a58659 100644 --- a/bevy_editor_panes/bevy_properties_pane/Cargo.toml +++ b/bevy_editor_panes/bevy_properties_pane/Cargo.toml @@ -5,6 +5,9 @@ edition = "2021" [dependencies] bevy.workspace = true +bevy_pane_layout.workspace = true +bevy_entity_inspector.workspace = true +bevy_editor_styles.workspace = true [lints] workspace = true diff --git a/bevy_editor_panes/bevy_properties_pane/src/lib.rs b/bevy_editor_panes/bevy_properties_pane/src/lib.rs index 6ef72e49..d1b7919f 100644 --- a/bevy_editor_panes/bevy_properties_pane/src/lib.rs +++ b/bevy_editor_panes/bevy_properties_pane/src/lib.rs @@ -1,19 +1,47 @@ -//! An interactive, reflection-based inspector for Bevy ECS data in running applications. -//! -//! Data can be viewed and modified in real-time, with changes being reflected in the application. +//! 3D Viewport for Bevy +use bevy::prelude::*; +use bevy_entity_inspector::{EntityInspector, EntityInspectorPlugin}; +use bevy_pane_layout::{prelude::{PaneAppExt, PaneStructure}, PaneContentNode}; -/// an add function that adds two numbers -pub fn add(left: u64, right: u64) -> u64 { - left + right +pub use bevy_entity_inspector::InspectedEntity; + +/// The identifier for the 3D Viewport. +/// This is present on any pane that is a 3D Viewport. +#[derive(Component)] +pub struct BevyPropertiesPane; + +impl Default for BevyPropertiesPane { + fn default() -> Self { + BevyPropertiesPane + } } -#[cfg(test)] -mod tests { - use super::*; +/// Plugin for the 3D Viewport pane. +pub struct PropertiesPanePlugin; + +impl Plugin for PropertiesPanePlugin { + fn build(&self, app: &mut App) { + app.add_plugins(EntityInspectorPlugin); - #[test] - fn it_works() { - let result = add(2, 2); - assert_eq!(result, 4); + app.register_pane("Properties", on_pane_creation); } } + +fn on_pane_creation( + structure: In, + mut commands: Commands +) { + println!("Properties pane created"); + + let content_node = structure.content; + + commands.entity(content_node).insert(( + EntityInspector, + Node { + width: Val::Auto, + flex_grow: 0.0, + overflow: Overflow::scroll_y(), + ..default() + } + )); +} diff --git a/bevy_widgets/bevy_collapsing_header/Cargo.toml b/bevy_widgets/bevy_collapsing_header/Cargo.toml new file mode 100644 index 00000000..0e0c2722 --- /dev/null +++ b/bevy_widgets/bevy_collapsing_header/Cargo.toml @@ -0,0 +1,12 @@ +[package] +name = "bevy_collapsing_header" +version = "0.1.0" +edition = "2021" + +[dependencies] +bevy.workspace = true +bevy_incomplete_bsn.workspace = true +bevy_editor_styles.workspace = true + +[lints] +workspace = true diff --git a/bevy_widgets/bevy_collapsing_header/examples/collapsing_header.rs b/bevy_widgets/bevy_collapsing_header/examples/collapsing_header.rs new file mode 100644 index 00000000..ccbb9566 --- /dev/null +++ b/bevy_widgets/bevy_collapsing_header/examples/collapsing_header.rs @@ -0,0 +1,37 @@ +//! This example demonstrates how to use the collapsing header widget with EntityDiffTree. + +use bevy::prelude::*; +use bevy_collapsing_header::*; +use bevy_incomplete_bsn::entity_diff_tree::{DiffTree, DiffTreeCommands}; + +fn main() { + App::new() + .add_plugins(DefaultPlugins) + .add_plugins(CollapsingHeaderPlugin) + .add_systems(Startup, setup) + .run(); +} + +fn setup(mut commands: Commands) { + commands.spawn(Camera2d::default()); + + let tree = DiffTree::new() + .with_patch_fn(|header: &mut CollapsingHeader| { + header.text = "Hello, collapsing header!".to_string(); + }) + .with_patch_fn(|color: &mut BackgroundColor| { + *color = BackgroundColor(Color::srgb(0.1, 0.1, 0.1)); + }) + .with_child(DiffTree::new()) + .with_child(DiffTree::new().with_patch_fn(|text: &mut Text| { + text.0 = "Content 1".to_string(); + })) + .with_child(DiffTree::new().with_patch_fn(|text: &mut Text| { + text.0 = "Content 2".to_string(); + })) + .with_child(DiffTree::new().with_patch_fn(|text: &mut Text| { + text.0 = "Content 3".to_string(); + })); + + commands.spawn_empty().diff_tree(tree); +} diff --git a/bevy_widgets/bevy_collapsing_header/examples/collapsing_header_raw.rs b/bevy_widgets/bevy_collapsing_header/examples/collapsing_header_raw.rs new file mode 100644 index 00000000..afb9efd5 --- /dev/null +++ b/bevy_widgets/bevy_collapsing_header/examples/collapsing_header_raw.rs @@ -0,0 +1,32 @@ +//! This example demonstrates how to use the collapsing header widget. + +use bevy::prelude::*; +use bevy_collapsing_header::*; + +fn main() { + App::new() + .add_plugins(DefaultPlugins) + .add_plugins(CollapsingHeaderPlugin) + .add_systems(Startup, setup) + .run(); +} + +fn setup(mut commands: Commands) { + commands.spawn(Camera2d::default()); + + commands + .spawn(( + CollapsingHeader::new("Hello, collapsing header!"), + BackgroundColor(Color::srgb(0.1, 0.1, 0.1)), + )) + .with_children(|cmd| { + cmd.spawn((Text::new(""), CollapsingHeaderText)); + + cmd.spawn((CollapsingHeaderContent, Node::default())) + .with_children(|cmd| { + cmd.spawn(Text::new("Content 1")); + cmd.spawn(Text::new("Content 2")); + cmd.spawn(Text::new("Content 3")); + }); + }); +} diff --git a/bevy_widgets/bevy_collapsing_header/src/lib.rs b/bevy_widgets/bevy_collapsing_header/src/lib.rs new file mode 100644 index 00000000..868e52f9 --- /dev/null +++ b/bevy_widgets/bevy_collapsing_header/src/lib.rs @@ -0,0 +1,171 @@ +//! This crate provides a collapsing header widget for Bevy. + +pub use bevy::prelude::*; +use bevy_editor_styles::Theme; +use bevy_incomplete_bsn::{children_patcher::*, entity_diff_tree::DiffTree}; + +/// A plugin that adds collapsing header functionality to the Bevy UI. +pub struct CollapsingHeaderPlugin; + +impl Plugin for CollapsingHeaderPlugin { + fn build(&self, app: &mut App) { + app.register_type::(); + + app.add_observer(on_header_click); + app.add_systems(PreUpdate, on_header_change); + app.add_systems(PreUpdate, update_header_font); + } +} + +/// A component that represents a collapsing header widget. +/// +/// This struct provides functionality for creating and managing a collapsible header +/// in a Bevy UI. It allows for toggling the visibility of its child elements. +#[derive(Component, Reflect, Clone)] +#[reflect(Component, ChildrenPatcher, Default)] +#[require(Node)] +pub struct CollapsingHeader { + /// The text to display in the header. + pub text: String, + /// A boolean flag indicating whether the header is collapsed (true) or expanded (false). + pub is_collapsed: bool, +} + +impl Default for CollapsingHeader { + fn default() -> Self { + Self { + is_collapsed: true, + text: "".to_string(), + } + } +} + +impl CollapsingHeader { + /// Create a new collapsing header with the given text. + pub fn new(text: impl Into) -> Self { + Self { + text: text.into(), + is_collapsed: true, + } + } +} + +impl ChildrenPatcher for CollapsingHeader { + fn children_patch(&mut self, children: &mut Vec) { + + info!("Patching children for header {:?}. Children count: {}", &self.text, children.len()); + + let move_text = self.text.clone(); + let collapsed = self.is_collapsed; + let header = DiffTree::new() + .with_patch_fn(move |text: &mut Text| { + let pred = if collapsed { + format!("+ {}", move_text.clone()) + } else { + format!("- {}", move_text.clone()) + }; + text.0 = pred; + }) + .with_patch_fn(|_: &mut CollapsingHeaderText| {}); + + let collapsed = self.is_collapsed; + let mut collapsable = DiffTree::new() + .with_patch_fn(move |node: &mut Node| { + if collapsed { + node.height = Val::Px(0.0); + } else { + node.height = Val::Auto; + } + node.display = Display::Flex; + node.flex_direction = FlexDirection::Column; + node.overflow = Overflow::clip(); + }) + .with_patch_fn(|_: &mut CollapsingHeaderContent| {}); + + for child in children.drain(..) { + collapsable.children.push(child); + } + + children.push(header); + children.push(collapsable); + } +} + +#[derive(Component, Default, Clone)] +pub struct CollapsingHeaderText; + +#[derive(Component, Default, Clone)] +pub struct CollapsingHeaderContent; + +fn on_header_click( + trigger: Trigger>, + mut q_headers: Query<&mut CollapsingHeader>, + q_parents: Query<&Parent, With>, +) { + let entity = trigger.entity(); + let Ok(header_entity) = q_parents.get(entity).map(|p| p.get()) else { + return; + }; + + let Ok(mut header) = q_headers.get_mut(header_entity) else { + return; + }; + + header.is_collapsed = !header.is_collapsed; +} + +fn on_header_change( + mut q_nodes: Query<&mut Node>, + mut q_texts: Query<&mut Text>, + q_changed: Query<(Entity, &CollapsingHeader, &Children), Changed>, +) { + for (entity, header, children) in q_changed.iter() { + { + let Ok(mut header_node) = q_nodes.get_mut(entity) else { + continue; + }; + + header_node.display = Display::Flex; + header_node.flex_direction = FlexDirection::Column; + } + + { + let Ok(mut content_node) = q_nodes.get_mut(children[1]) else { + continue; + }; + + if header.is_collapsed { + content_node.height = Val::Px(0.0); + } else { + content_node.height = Val::Auto; + } + content_node.display = Display::Flex; + content_node.flex_direction = FlexDirection::Column; + content_node.overflow = Overflow::clip(); + } + + let Ok(mut text) = q_texts.get_mut(children[0]) else { + continue; + }; + + let pred = if header.is_collapsed { + format!("+ {}", header.text.clone()) + } else { + format!("- {}", header.text.clone()) + }; + text.0 = pred; + } +} + +fn update_header_font( + mut q_changed: Query< + (&mut TextFont, &mut TextColor), + (Changed, With), + >, + theme: Res, +) { + for (mut font, mut color) in q_changed.iter_mut() { + font.font = theme.text.font.clone(); + color.0 = theme.text.text_color; + } +} diff --git a/bevy_widgets/bevy_entity_inspector/Cargo.toml b/bevy_widgets/bevy_entity_inspector/Cargo.toml new file mode 100644 index 00000000..144db4c4 --- /dev/null +++ b/bevy_widgets/bevy_entity_inspector/Cargo.toml @@ -0,0 +1,14 @@ +[package] +name = "bevy_entity_inspector" +version = "0.1.0" +edition = "2021" + +[dependencies] +bevy.workspace = true +bevy_field_forms.workspace = true +bevy_incomplete_bsn.workspace = true +bevy_collapsing_header.workspace = true +bevy_editor_styles.workspace = true + +[lints] +workspace = true diff --git a/bevy_widgets/bevy_entity_inspector/examples/transform_inspector.rs b/bevy_widgets/bevy_entity_inspector/examples/transform_inspector.rs new file mode 100644 index 00000000..2fcdde78 --- /dev/null +++ b/bevy_widgets/bevy_entity_inspector/examples/transform_inspector.rs @@ -0,0 +1,48 @@ +//! This example demonstrates how to use the `EntityInspector` plugin to inspect the transform of an entity. + +use bevy::prelude::*; +use bevy_entity_inspector::{EntityInspector, EntityInspectorPlugin, InspectedEntity}; + +fn main() { + App::new() + .add_plugins(DefaultPlugins) + .add_plugins(EntityInspectorPlugin) + .add_systems(Startup, setup) + .run(); +} + +fn setup( + mut commands: Commands, + mut meshes: ResMut>, + mut materials: ResMut>, +) { + commands.spawn(( + Camera3d::default(), + Transform::from_translation(Vec3::splat(5.0)).looking_at(Vec3::ZERO, Vec3::Y), + )); + + commands.spawn(( + Transform::from_translation(Vec3::new(1.0, 2.0, 3.0)).looking_at(Vec3::ZERO, Vec3::Y), + DirectionalLight::default(), + )); + + commands.spawn(( + Transform::default(), + Mesh3d(meshes.add(Cuboid::from_length(1.0))), + MeshMaterial3d(materials.add(StandardMaterial::default())), + InspectedEntity, + )); + + commands.spawn(( + EntityInspector, + Node { + width: Val::Auto, + border: UiRect::all(Val::Px(1.0)), + overflow: Overflow::scroll_y(), + ..default() + }, + BorderRadius::all(Val::Px(5.0)), + BorderColor(Color::srgb(0.5, 0.5, 0.5)), + BackgroundColor(Color::srgb(0.1, 0.1, 0.1)), + )); +} diff --git a/bevy_widgets/bevy_entity_inspector/src/lib.rs b/bevy_widgets/bevy_entity_inspector/src/lib.rs new file mode 100644 index 00000000..bd247bf3 --- /dev/null +++ b/bevy_widgets/bevy_entity_inspector/src/lib.rs @@ -0,0 +1,48 @@ +//! This crate provides a entity inspector pane for Bevy Editor + +use bevy::prelude::*; +use bevy_collapsing_header::CollapsingHeaderPlugin; +use bevy_field_forms::FieldFormsPlugin; +use render::ChangeComponentField; +use render_impl::RenderStorage; + +pub mod render; +pub mod render_impl; + +/// Plugin for the entity inspector. +pub struct EntityInspectorPlugin; + +impl Plugin for EntityInspectorPlugin { + fn build(&self, app: &mut App) { + if !app.is_plugin_added::() { + app.add_plugins(FieldFormsPlugin); + } + + if !app.is_plugin_added::() { + app.add_plugins(CollapsingHeaderPlugin); + } + + if !app.is_plugin_added::() { + app.add_plugins(bevy_editor_styles::StylesPlugin); + } + + app.add_event::(); + + app.init_resource::(); + app.add_plugins(render_impl::RenderImplPlugin); + + app.add_systems(PreUpdate, render::render_entity_inspector); + app.add_systems(PreUpdate, render::render_component_inspector); + + app.add_observer(render::on_change_component_field); + } +} + +/// A marker for node in whicj entity inspector will render sub-tree + +#[derive(Component)] +pub struct EntityInspector; + +/// Component for marking an entity as being inspected. +#[derive(Component)] +pub struct InspectedEntity; diff --git a/bevy_widgets/bevy_entity_inspector/src/render.rs b/bevy_widgets/bevy_entity_inspector/src/render.rs new file mode 100644 index 00000000..94e4b5b4 --- /dev/null +++ b/bevy_widgets/bevy_entity_inspector/src/render.rs @@ -0,0 +1,509 @@ +//! Contains the system to render sub-tree for an entity inspector + +#![allow(unsafe_code)] + +use std::{any::TypeId, sync::Arc}; + +use bevy::{ + ecs::{ + component::{ComponentId, Components}, + system::SystemChangeTick, + }, + prelude::*, + reflect::ReflectFromPtr, +}; +use bevy_collapsing_header::CollapsingHeader; +use bevy_editor_styles::Theme; +use bevy_incomplete_bsn::entity_diff_tree::{DiffTree, DiffTreeCommands}; + +use crate::{render_impl::RenderStorage, EntityInspector, InspectedEntity}; + +#[allow(dead_code)] +/// Context for rendering a component in the entity inspector. +pub struct RenderContext<'w, 's> { + /// Storage for render-related data. + render_storage: &'w RenderStorage, + /// The entity being inspected. + entity: Entity, + /// The ID of the component being rendered. + component_id: ComponentId, + /// Theme to unify style + theme: &'s Theme, +} + +/// Component for managing the inspection of a specific component. +#[derive(Component)] +#[require(Node)] +pub struct ComponentInspector { + /// The ID of the component being inspected. + pub component_id: ComponentId, + /// The type ID of the component. + pub type_id: TypeId, + /// Flag indicating whether the component has been rendered. + pub rendered: bool, +} + +/// Component representing a change to a field in a component. +#[derive(Component, Clone)] +pub struct ChangeComponentField { + /// The path to the field being changed. + pub path: String, + /// The new value for the field. + pub value: Arc, + /// Optional function for directly applying the change to the component. + pub direct_cange: + Option>, +} + +impl Event for ChangeComponentField { + type Traversal = &'static Parent; + const AUTO_PROPAGATE: bool = true; +} + +/// Renders the inspector for a specific component of an entity. +/// +/// This system is responsible for updating the visual representation of a component +/// in the entity inspector UI when the component's data changes. +/// +/// # Behavior +/// +/// 1. Retrieves the currently inspected entity. +/// 2. Iterates through all ComponentInspector entities. +/// 3. For each ComponentInspector: +/// - Checks if the corresponding component on the inspected entity has changed. +/// - If changed, retrieves the component data and type information. +/// - Creates an EntityDiffTree to represent the updated UI for the component. +/// - Applies the updated UI to the inspector entity. +/// +/// This system ensures that the entity inspector UI stays up-to-date with any changes +/// to the inspected entity's components, providing real-time feedback in the editor. + +pub fn render_component_inspector( + mut commands: Commands, + mut q_inspector: Query<(Entity, &mut ComponentInspector), Without>, + q_inspected: Query>, + app_registry: Res, + system_change_ticks: SystemChangeTick, + render_storage: Res, + theme: Res, +) { + let Ok(inspected_entity) = q_inspected.get_single() else { + warn!("No inspected entity or many found"); + return; + }; + + for (inspector_entity, mut inspector) in q_inspector.iter_mut() { + let Some(change_ticks) = inspected_entity.get_change_ticks_by_id(inspector.component_id) + else { + warn!("No change ticks found for component: {:?}", inspector.component_id); + continue; + }; + + if !change_ticks.is_changed( + system_change_ticks.last_run(), + system_change_ticks.this_run(), + ) && inspector.rendered + { + // info!("Component not changed for component: {:?}, skipping render", inspector.component_id); + continue; + } + + // Component was changed, render it + + let type_registry = app_registry.read(); + + let Some(reg) = type_registry.get(inspector.type_id) else { + // warn!("No type registry found for type: {:?}", inspector.type_id); + continue; + }; + + let Some(reflect_from_ptr) = reg.data::() else { + warn!("No ReflectFromPtr found for type: {:?}", inspector.type_id); + continue; + }; + + let Ok(component_data) = inspected_entity.get_by_id(inspector.component_id) else { + warn!("No component data found for component: {:?}", inspector.component_id); + continue; + }; + + let reflected_data = unsafe { reflect_from_ptr.from_ptr()(component_data) }; + + let mut tree = DiffTree::new().with_patch_fn(|node: &mut Node| { + node.flex_direction = FlexDirection::Column; + node.max_width = Val::Px(300.0); + }); + + let name = reg + .type_info() + .type_path() + .split("::") + .last() + .unwrap_or_default(); + + // Remove the generic type from the name if it exists + let name = name.replace(">", ""); + + let render_context = RenderContext { + render_storage: &render_storage, + entity: inspected_entity.id(), + component_id: inspector.component_id, + theme: &theme, + }; + + let reflect_content = recursive_reflect_render( + reflected_data.as_partial_reflect(), + format!(""), // The string reflect path starts with a dot + &render_context, + ); + + tree.add_child( + DiffTree::new() + .with_patch_fn(move |collapsing_header: &mut CollapsingHeader| { + collapsing_header.text = name.to_string(); + }) + .with_patch_fn(|text_layout: &mut TextLayout| { + text_layout.linebreak = LineBreak::AnyCharacter; + }) + .with_patch_fn(|node: &mut Node| { + node.max_width = Val::Px(300.0); + }) + .with_child(reflect_content), + ); + + let id = inspector.component_id; + + // tree.add_child( + // DiffTree::new() + // .with_patch_fn(move |text: &mut Text| { + // text.0 = format!("Component: {:?}", id); + // }) + // ); + + let font_cloned = theme.text.font.clone(); + let color_cloned = theme.text.text_color; + tree.add_cascade_patch_fn::(move |font: &mut TextFont| { + font.font = font_cloned.clone(); + font.font_size = 14.0; + }); + tree.add_cascade_patch_fn::(move |color: &mut TextColor| { + color.0 = color_cloned.clone(); + }); + + commands.entity(inspector_entity).diff_tree(tree); + + inspector.rendered = true; + } +} + +/// Observer for change component field event +pub fn on_change_component_field( + trigger: Trigger, + q_component_inspectors: Query<&ComponentInspector, Without>, + mut q_inspected: Query>, + app_registry: Res, +) { + let entity = trigger.entity(); + let Ok(inspector) = q_component_inspectors.get(entity) else { + return; + }; + + let Ok(mut inspected_entity) = q_inspected.get_single_mut() else { + error!("No inspected entity found"); + return; + }; + + let type_registry = app_registry.read(); + + let Some(reg) = type_registry.get(inspector.type_id) else { + error!("No type registry found for type: {:?}", inspector.type_id); + return; + }; + + let Some(reflect_from_ptr) = reg.data::() else { + error!("No ReflectFromPtr found for type: {:?}", inspector.type_id); + return; + }; + + let Ok(mut component_data) = inspected_entity.get_mut_by_id(inspector.component_id) else { + error!("Failed to get component data"); + return; + }; + + { + let reflected_data = unsafe { reflect_from_ptr.from_ptr_mut()(component_data.as_mut()) }; + + let Ok(field) = reflected_data.reflect_path_mut(trigger.path.as_str()) else { + error!("Failed to reflect path: {:?}", trigger.path); + return; + }; + + if let Some(direct_change) = trigger.direct_cange.as_ref() { + info!("Apply direct change to field: {:?}", trigger.path); + direct_change(field, trigger.value.as_ref()); + } else { + info!("Apply value to field: {:?}", trigger.path); + field.apply(trigger.value.as_ref()); + } + } + + component_data.set_changed(); +} + +/// Render the entity inspector +pub fn render_entity_inspector( + mut commands: Commands, + q_inspected: Query>, + mut q_inspector: Query<(Entity, Option<&Children>, &mut Node), (Without, With)>, + q_component_inspectors: Query<&ComponentInspector>, + components: &Components, +) { + let Ok(inspected_entity) = q_inspected.get_single() else { + if q_inspector.is_empty() { + warn!("No inspected entity found"); + } else { + warn!("Multiple inspected entities found"); + } + return; + }; + + for (inspector, children, mut node) in q_inspector.iter_mut() { + let entity = inspected_entity.id(); + + node.display = Display::Flex; + node.flex_direction = FlexDirection::Column; + node.overflow = Overflow::scroll(); + node.height = Val::Percent(100.0); + + // let mut tree = DiffTree::new(); + + // tree.add_patch_fn(|node: &mut Node| { + // node.display = Display::Flex; + // node.flex_direction = FlexDirection::Column; + // node.overflow = Overflow::scroll(); + // node.height = Val::Percent(100.0); + // }); + + // tree.add_patch_fn(|_: &mut Interaction| {}); + + // tree.add_child(DiffTree::new().with_patch_fn(move |text: &mut Text| { + // text.0 = format!("Entity: {}", entity); + // })); + // commands.entity(inspector).diff_tree(tree); + + let mut compenent_id_set = inspected_entity + .archetype() + .components() + .collect::>(); + + let mut found_component_ids = Vec::new(); + + if let Some(children) = children { + for child in children.iter() { + let Ok(component_inspector) = q_component_inspectors.get(*child) else { + continue; + }; + + if compenent_id_set.contains(&component_inspector.component_id) { + found_component_ids.push(component_inspector.component_id); + } else { + // Component is not attached to the entity anymore, remove it + info!( + "Component is not attached to the entity anymore, removing it: {:?}", + component_inspector.component_id + ); + commands.entity(*child).despawn_recursive(); + } + } + } + + // Find the components that are not represented in the inspector + compenent_id_set.retain(|id| !found_component_ids.contains(id)); + // Add new inspectors for the remaining components + for component_id in compenent_id_set.iter() { + let Some(info) = components.get_info(*component_id) else { + continue; + }; + + let Some(type_id) = info.type_id() else { + continue; + }; + + let component_inspector_entity = commands + .spawn(ComponentInspector { + component_id: *component_id, + type_id, + rendered: false, + }) + .id(); + + commands + .entity(inspector) + .add_child(component_inspector_entity); + } + + } +} + +fn recursive_reflect_render( + data: &dyn PartialReflect, + path: String, + render_context: &RenderContext, +) -> DiffTree { + if let Some(render_fn) = render_context + .render_storage + .renders + .get(&data.get_represented_type_info().unwrap().type_id()) + { + return render_fn(data, path, render_context); + } else { + let mut tree = DiffTree::new(); + tree.add_patch_fn(|node: &mut Node| { + node.display = Display::Flex; + node.flex_direction = FlexDirection::Column; + }); + match data.reflect_ref() { + bevy::reflect::ReflectRef::Struct(v) => { + for field_idx in 0..v.field_len() { + let field = v.field_at(field_idx).unwrap(); + let name = v.name_at(field_idx).unwrap_or_default().to_string(); + if field.reflect_ref().as_opaque().is_ok() { + // Opaque fields are rendered as a row + let mut row = DiffTree::new().with_patch_fn(|node: &mut Node| { + node.flex_direction = FlexDirection::Row; + }); + let moving_name = name.clone(); + row.add_child( + DiffTree::new() + .with_patch_fn(move |text: &mut Text| { + text.0 = format!("{}", moving_name); + }) + .with_patch_fn(|node: &mut Node| { + node.padding = UiRect::all(Val::Px(2.0)); + }), + ); + row.add_child(recursive_reflect_render( + field, + format!("{}.{}", path, name), + render_context, + )); + tree.add_child(row); + } else { + // Other fields are rendered as a column with a shift + let moving_name = name.clone(); + tree.add_child( + DiffTree::new() + .with_patch_fn(move |text: &mut Text| { + text.0 = format!("{}", moving_name); + }) + .with_patch_fn(|node: &mut Node| { + node.margin = UiRect::all(Val::Px(5.0)); + }), + ); + + let mut row = DiffTree::new().with_patch_fn(|node: &mut Node| { + node.flex_direction = FlexDirection::Row; + }); + + // Add tab + row.add_child(DiffTree::new().with_patch_fn(|node: &mut Node| { + node.width = Val::Px(20.0); + })); + + row.add_child(recursive_reflect_render( + field, + format!("{}.{}", path, name), + render_context, + )); + + tree.add_child(row); + } + } + } + bevy::reflect::ReflectRef::TupleStruct(v) => { + for (idx, field) in v.iter_fields().enumerate() { + tree.add_child(recursive_reflect_render( + field, + format!("{}[{}]", path, idx), + render_context, + )); + } + } + bevy::reflect::ReflectRef::Tuple(v) => { + for (idx, field) in v.iter_fields().enumerate() { + tree.add_child(recursive_reflect_render( + field, + format!("{}[{}]", path, idx), + render_context, + )); + } + } + bevy::reflect::ReflectRef::List(v) => { + for (idx, field) in v.iter().enumerate() { + tree.add_child(recursive_reflect_render( + field, + format!("{}[{}]", path, idx), + render_context, + )); + } + } + bevy::reflect::ReflectRef::Array(v) => { + for (idx, field) in v.iter().enumerate() { + tree.add_child(recursive_reflect_render( + field, + format!("{}[{}]", path, idx), + render_context, + )); + } + } + bevy::reflect::ReflectRef::Map(v) => { + for (_, field) in v.iter() { + tree.add_child(recursive_reflect_render( + field, + format!("{}", path), + render_context, + )); + } + } + bevy::reflect::ReflectRef::Set(v) => { + for field in v.iter() { + tree.add_child(recursive_reflect_render( + field, + format!("{}", path), + render_context, + )); + } + } + bevy::reflect::ReflectRef::Enum(v) => { + for field in v.iter_fields() { + tree.add_child(recursive_reflect_render( + field.value(), + format!("{}", path), + render_context, + )); + } + } + bevy::reflect::ReflectRef::Opaque(v) => { + let v = v.clone_value(); + let font_cloned = render_context.theme.text.font.clone(); + let color_cloned = render_context.theme.text.text_color; + tree.add_child( + DiffTree::new() + .with_patch_fn(move |text: &mut Text| { + text.0 = format!("{:?}", v); + }) + .with_patch_fn(move |font: &mut TextFont| { + font.font = font_cloned.clone(); + }) + .with_patch_fn(move |color: &mut TextColor| { + color.0 = color_cloned.clone(); + }) + .with_patch_fn(|node: &mut Node| { + node.padding = UiRect::all(Val::Px(2.0)); + }), + ); + } + } + tree + } +} diff --git a/bevy_widgets/bevy_entity_inspector/src/render_impl/float_impl.rs b/bevy_widgets/bevy_entity_inspector/src/render_impl/float_impl.rs new file mode 100644 index 00000000..8d503415 --- /dev/null +++ b/bevy_widgets/bevy_entity_inspector/src/render_impl/float_impl.rs @@ -0,0 +1,78 @@ +//! Implementation for rendering a float value in the entity inspector + +use std::sync::Arc; + +use bevy::prelude::*; + +use bevy_field_forms::{ + drag_input::DragInput, + input_field::{InputField, ValueChanged}, + validate_highlight::SimpleBorderHighlight, +}; +use bevy_incomplete_bsn::entity_diff_tree::DiffTree; + +use crate::render::{ChangeComponentField, RenderContext}; + +/// Implementation for rendering a float value in the entity inspector +pub fn float_render_impl(float: &f32, path: String, _: &RenderContext) -> DiffTree { + let mut tree = DiffTree::new(); + + let val = *float; //Clone the value to avoid borrowing issues + + tree.add_patch_fn(move |input: &mut InputField| { + input.value = val; + input.controlled = true; // Value of input field is controlled by the inspector + }); + + tree.add_patch_fn(|_: &mut DragInput| {}); + + tree.add_patch_fn(|node: &mut Node| { + node.min_width = Val::Px(100.0); + node.height = Val::Px(18.0); + node.border = UiRect::all(Val::Px(1.0)); + node.padding = UiRect::all(Val::Px(1.0)); + }); + + tree.add_patch_fn(|background: &mut BackgroundColor| { + *background = BackgroundColor(Color::srgb(0.2, 0.2, 0.2)); + }); + + tree.add_patch_fn(|border: &mut BorderColor| { + *border = BorderColor(Color::srgb(0.3, 0.3, 0.3)); + }); + + tree.add_patch_fn(|border_radius: &mut BorderRadius| { + *border_radius = BorderRadius::all(Val::Px(5.0)); + }); + + tree.add_patch_fn(|_: &mut SimpleBorderHighlight| {}); + + tree.add_patch_fn(|text_font: &mut TextFont| { + text_font.font_size = 14.0; + }); + + tree.add_observer_patch( + move |trigger: Trigger>, mut commands: Commands| { + info!( + "Trigger reflect change with path: {} and value: {}", + path, trigger.0 + ); + let entity = trigger.entity(); + + commands.trigger_targets( + ChangeComponentField { + value: Arc::new(trigger.0), + path: path.clone(), + direct_cange: Some(Arc::new(|dst, src| { + let dst_f32 = dst.try_downcast_mut::().unwrap(); + let src_f32 = src.try_downcast_ref::().unwrap(); + *dst_f32 = *src_f32; + })), + }, + entity, + ); + }, + ); + + tree +} diff --git a/bevy_widgets/bevy_entity_inspector/src/render_impl/mod.rs b/bevy_widgets/bevy_entity_inspector/src/render_impl/mod.rs new file mode 100644 index 00000000..218b9b6b --- /dev/null +++ b/bevy_widgets/bevy_entity_inspector/src/render_impl/mod.rs @@ -0,0 +1,58 @@ +//! This crate contains the implementation of some common components or types that are used in the entity inspector + +pub mod float_impl; + +use std::any::{Any, TypeId}; + +use bevy::{prelude::*, utils::HashMap}; +use bevy_incomplete_bsn::entity_diff_tree::DiffTree; + +use crate::render::RenderContext; + +/// Plugin for store implementation for type to be rendered in entity inspector +pub struct RenderImplPlugin; + +impl Plugin for RenderImplPlugin { + fn build(&self, app: &mut App) { + app.add_render_impl::(float_impl::float_render_impl); + } +} + +/// Storage for render-related data. +#[derive(Resource, Default)] +pub struct RenderStorage { + /// Map of type IDs to render functions. + pub renders: HashMap< + TypeId, + Box< + dyn Fn(&dyn PartialReflect, String, &RenderContext) -> DiffTree + Send + Sync + 'static, + >, + >, +} + +/// Trait for adding render implementations to an application. +pub trait RenderStorageApp { + /// Adds a render implementation for a type to the render storage. + fn add_render_impl( + &mut self, + render_fn: impl Fn(&T, String, &RenderContext) -> DiffTree + Send + Sync + 'static, + ) -> &mut Self; +} + +impl RenderStorageApp for App { + fn add_render_impl( + &mut self, + render_fn: impl Fn(&T, String, &RenderContext) -> DiffTree + Send + Sync + 'static, + ) -> &mut Self { + self.world_mut() + .resource_mut::() + .renders + .insert( + TypeId::of::(), + Box::new(move |untyped, path, context| { + render_fn(untyped.try_downcast_ref::().unwrap(), path, context) + }), + ); + self + } +} diff --git a/bevy_widgets/bevy_field_forms/Cargo.toml b/bevy_widgets/bevy_field_forms/Cargo.toml new file mode 100644 index 00000000..50168704 --- /dev/null +++ b/bevy_widgets/bevy_field_forms/Cargo.toml @@ -0,0 +1,13 @@ +[package] +name = "bevy_field_forms" +version = "0.1.0" +edition = "2021" + +[dependencies] +bevy.workspace = true +bevy_text_editing.workspace = true +bevy_focus.workspace = true +bevy_i-cant-believe-its-not-bsn.workspace = true + +[lints] +workspace = true diff --git a/bevy_widgets/bevy_field_forms/examples/nickname.rs b/bevy_widgets/bevy_field_forms/examples/nickname.rs new file mode 100644 index 00000000..9ecc56a3 --- /dev/null +++ b/bevy_widgets/bevy_field_forms/examples/nickname.rs @@ -0,0 +1,123 @@ +//! This example demonstrates how to use the `ValidatedInputFieldPlugin` to create a validated input field for a character name. + +use bevy::{prelude::*, utils::HashSet}; +use bevy_field_forms::{ + input_field::{InputField, InputFieldPlugin, Validable, ValidationChanged, ValidationState}, + validate_highlight::SimpleBorderHighlight, + FieldFormsPlugin, +}; + +fn main() { + App::new() + .add_plugins(DefaultPlugins) + .add_plugins(FieldFormsPlugin) + .add_plugins(InputFieldPlugin::::default()) + .add_observer(on_validation_changed) + .add_systems(Startup, setup) + .run(); +} + +fn setup(mut commands: Commands) { + commands.spawn(Camera2d::default()); + + let text_msg_entity = commands + .spawn(( + Text::new(""), + TextColor(Color::srgb(1.0, 0.0, 0.0)), + TextFont { + font_size: 12.0, + ..Default::default() + }, + )) + .id(); + + commands + .spawn(Node { + display: Display::Flex, + flex_direction: FlexDirection::Column, + align_items: AlignItems::Center, + justify_content: JustifyContent::Center, + width: Val::Percent(100.0), + height: Val::Percent(100.0), + ..default() + }) + .with_children(move |cmd| { + cmd.spawn(Text::new("Nickname:")); + cmd.spawn(( + Node { + border: UiRect::all(Val::Px(1.0)), + width: Val::Px(300.0), + height: Val::Px(25.0), + ..default() + }, + BorderRadius::all(Val::Px(5.0)), + BorderColor(Color::WHITE), + InputField::new(CharacterName(String::new())), + SimpleBorderHighlight::default(), + CharacterValidator { + msg_text: text_msg_entity, + }, + )); + }) + .add_child(text_msg_entity); +} + +#[derive(Clone, Debug, PartialEq, Eq, Default)] +struct CharacterName(pub String); + +impl Validable for CharacterName { + fn validate(text: &str) -> Result { + let allowed_chars = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789_" + .chars() + .collect::>(); + if text.chars().all(|c| allowed_chars.contains(&c)) { + Ok(CharacterName(text.to_string())) + } else { + let invalid_chars: String = text + .chars() + .filter(|c| !allowed_chars.contains(c)) + .collect(); + Err(format!("Invalid character name. The following characters are not allowed: '{}'. Only letters, numbers, and underscores can be used.", invalid_chars)) + } + } +} + +impl ToString for CharacterName { + fn to_string(&self) -> String { + self.0.clone() + } +} + +#[derive(Component)] +struct CharacterValidator { + msg_text: Entity, +} + +fn on_validation_changed( + trigger: Trigger, + mut commands: Commands, + q_character_validator: Query<&CharacterValidator>, +) { + let entity = trigger.entity(); + let Ok(character_validator) = q_character_validator.get(entity) else { + return; + }; + + match &trigger.0 { + ValidationState::Valid => { + commands + .entity(character_validator.msg_text) + .insert(Text::new("")); + } + ValidationState::Invalid(msg) => { + commands + .entity(character_validator.msg_text) + .insert(Text::new(msg)); + } + ValidationState::Unchecked => { + commands + .entity(character_validator.msg_text) + .insert(Text::new("")); + } + } +} diff --git a/bevy_widgets/bevy_field_forms/examples/numeric_fields.rs b/bevy_widgets/bevy_field_forms/examples/numeric_fields.rs new file mode 100644 index 00000000..a173dc81 --- /dev/null +++ b/bevy_widgets/bevy_field_forms/examples/numeric_fields.rs @@ -0,0 +1,72 @@ +//! This example demonstrates how to use the `ValidatedInputFieldPlugin` to create a validated input field for a character name. + +use bevy::prelude::*; +use bevy_field_forms::{ + drag_input::{DragInput, Draggable}, + input_field::{InputField, Validable}, + validate_highlight::SimpleBorderHighlight, + FieldFormsPlugin, +}; + +fn main() { + App::new() + .add_plugins(DefaultPlugins) + .add_plugins(FieldFormsPlugin) + .add_systems(Startup, setup) + .run(); +} + +fn setup(mut commands: Commands) { + commands.spawn(Camera2d::default()); + + commands + .spawn(Node { + display: Display::Grid, + grid_template_columns: vec![ + RepeatedGridTrack::min_content(1), + RepeatedGridTrack::auto(1), + RepeatedGridTrack::min_content(1), + RepeatedGridTrack::auto(1), + ], + align_items: AlignItems::Center, + justify_content: JustifyContent::Center, + ..default() + }) + .with_children(move |cmd| { + spawn_numeric_field::(cmd, "i8"); + spawn_numeric_field::(cmd, "u8"); + spawn_numeric_field::(cmd, "i16"); + spawn_numeric_field::(cmd, "u16"); + spawn_numeric_field::(cmd, "i32"); + spawn_numeric_field::(cmd, "u32"); + spawn_numeric_field::(cmd, "i64"); + spawn_numeric_field::(cmd, "u64"); + spawn_numeric_field::(cmd, "i128"); + spawn_numeric_field::(cmd, "u128"); + spawn_numeric_field::(cmd, "f32"); + spawn_numeric_field::(cmd, "f64"); + }); +} + +fn spawn_numeric_field(cmd: &mut ChildBuilder, label: &str) { + cmd.spawn(( + Text::new(format!("{}:", label)), + Node { + margin: UiRect::all(Val::Px(5.0)), + ..default() + }, + )); + cmd.spawn(( + Node { + width: Val::Px(100.0), + height: Val::Px(25.0), + border: UiRect::all(Val::Px(1.0)), + margin: UiRect::all(Val::Px(5.0)), + ..Default::default() + }, + BackgroundColor(Color::srgb(0.2, 0.2, 0.2)), + InputField::::default(), + SimpleBorderHighlight::default(), + DragInput::::default(), + )); +} diff --git a/bevy_widgets/bevy_field_forms/src/drag_input.rs b/bevy_widgets/bevy_field_forms/src/drag_input.rs new file mode 100644 index 00000000..7dfa0aa7 --- /dev/null +++ b/bevy_widgets/bevy_field_forms/src/drag_input.rs @@ -0,0 +1,173 @@ +//! This module contains the logic allow to drag value stored in a input field + +use bevy::prelude::*; + +use crate::input_field::{InputField, Validable, ValueChanged}; + +/// Plugin for dragging a value stored in an input field +pub struct DragInputPlugin { + _marker: std::marker::PhantomData, +} + +impl Default for DragInputPlugin { + fn default() -> Self { + Self { + _marker: std::marker::PhantomData, + } + } +} + +impl Plugin for DragInputPlugin { + fn build(&self, app: &mut App) { + app.add_observer(on_drag::); + + app.add_systems(PreUpdate, on_interaction_changed::); + } +} + +/// A trait for values that can be dragged +pub trait Draggable: + Send + Sync + 'static + Default + PartialEq + Validable + std::ops::Add + Copy +{ + /// Converts a f32 value to Self + fn from_f32(value: f32) -> Self; + + /// Converts Self to a f32 value + fn into_f32(self) -> f32; + + /// Safely adds another value of the same type, handling potential overflows + fn safe_add(&self, other: Self) -> Self; + + /// Safely subtracts another value of the same type, handling potential underflows + fn safe_sub(&self, other: Self) -> Self; + + /// Returns the default ratio of the value change per logical pixel drag + fn default_drag_ratio() -> f32; +} + +/// A component that allows dragging a value stored in an input field +#[derive(Component, Clone)] +#[require(Node, InputField::, Interaction)] +pub struct DragInput { + /// The accumulated drag value + pub drag_accumulate: f32, + + /// The ratio of the value change per logical pixel drag + pub drag_ratio: f32, + + _marker: std::marker::PhantomData, +} + +impl Default for DragInput { + fn default() -> Self { + Self { + drag_accumulate: 0.0, + drag_ratio: T::default_drag_ratio(), + _marker: std::marker::PhantomData, + } + } +} + +fn on_drag( + trigger: Trigger>, + mut commands: Commands, + mut q_drag_inputs: Query<(&mut DragInput, &mut InputField)>, +) { + let entity = trigger.entity(); + + let Ok((mut drag_input, mut input_field)) = q_drag_inputs.get_mut(entity) else { + return; + }; + + let delta = trigger.delta.x; + drag_input.drag_accumulate += delta * drag_input.drag_ratio; + + let from_accumulated: T = T::from_f32(drag_input.drag_accumulate.abs()); + let accumulted_decrese: f32 = from_accumulated.into_f32(); + if accumulted_decrese != 0.0 { + let new_val: T; + if drag_input.drag_accumulate > 0.0 { + new_val = input_field.value.safe_add(from_accumulated); + drag_input.drag_accumulate -= accumulted_decrese; + } else { + new_val = input_field.value.safe_sub(from_accumulated); + drag_input.drag_accumulate += accumulted_decrese; + } + + commands.trigger_targets(ValueChanged(new_val), entity); + if !input_field.controlled { + input_field.value = new_val; + } + } +} + +fn on_interaction_changed( + mut q_changed_interactions: Query<(&mut DragInput, &Interaction), Changed>, +) { + for (mut drag_input, interaction) in q_changed_interactions.iter_mut() { + if *interaction != Interaction::Pressed { + drag_input.drag_accumulate = 0.0; + } + } +} + +macro_rules! impl_draggable_for_numeric { + ($($t:ty),*) => { + $( + impl Draggable for $t { + fn default_drag_ratio() -> f32 { + 0.1 + } + + fn safe_add(&self, other: Self) -> Self { + self.checked_add(other).unwrap_or(Self::MAX) + } + + fn safe_sub(&self, other: Self) -> Self { + self.checked_sub(other).unwrap_or(Self::MIN) + } + + fn from_f32(value: f32) -> Self { + let clamped = value.clamp(Self::MIN as f32, Self::MAX as f32); + clamped as Self + } + + fn into_f32(self) -> f32 { + self as f32 + } + } + )* + }; +} + +impl_draggable_for_numeric!(i8, i16, i32, i64, i128, u8, u16, u32, u64, u128); + +macro_rules! impl_draggable_for_float { + ($($t:ty),*) => { + $( + impl Draggable for $t { + fn default_drag_ratio() -> f32 { + 0.01 + } + + fn safe_add(&self, other: Self) -> Self { + *self + other + } + + fn safe_sub(&self, other: Self) -> Self { + *self - other + } + + fn from_f32(value: f32) -> Self { + value as Self + } + + fn into_f32(self) -> f32 { + self as f32 + } + } + )* + }; +} + +impl_draggable_for_float!(f32, f64); diff --git a/bevy_widgets/bevy_field_forms/src/input_field.rs b/bevy_widgets/bevy_field_forms/src/input_field.rs new file mode 100644 index 00000000..8dde5bb8 --- /dev/null +++ b/bevy_widgets/bevy_field_forms/src/input_field.rs @@ -0,0 +1,196 @@ +//! This module provides a validated input field with a generic value type. + +use bevy::prelude::*; +use bevy_text_editing::*; + +/// Plugin for validated input fields with a generic value type +pub struct InputFieldPlugin { + _marker: std::marker::PhantomData, +} + +impl Default for InputFieldPlugin { + fn default() -> Self { + Self { + _marker: std::marker::PhantomData, + } + } +} + +impl Plugin for InputFieldPlugin { + fn build(&self, app: &mut App) { + if !app.is_plugin_added::() { + app.add_plugins(EditableTextLinePlugin); + } + + app.add_event::(); + app.add_event::>(); + app.add_event::>(); + + app.add_systems(PostUpdate, on_value_changed::); + app.add_systems(PreUpdate, on_created::); + + app.add_observer(on_text_changed::); + } +} + +/// A text field with input validation +/// It will not contain special style updates for validation state, because it's expected that it will be +/// combined with other widgets to form a custom UI. +#[derive(Component, Clone)] +#[require(EditableTextLine(construct_editable_label))] +pub struct InputField { + /// The last valid value + pub value: T, + /// The current validation state + pub validation_state: ValidationState, + /// If true, this text field will not update its value automatically + /// and will require an external update call to update the value. + pub controlled: bool, + /// Old value + pub old_value: T, +} + +fn construct_editable_label() -> EditableTextLine { + EditableTextLine::controlled("") +} + +impl Default for InputField { + fn default() -> Self { + Self::new(T::default()) + } +} + +impl InputField { + /// Create a new validated input field with the given value + pub fn new(value: T) -> Self { + Self { + value: value.clone(), + validation_state: ValidationState::Unchecked, + controlled: false, + old_value: value, + } + } +} + +/// A trait for types that can be validated from a string input. +/// +/// Types implementing this trait can be used with `ValidatedInputField`. +pub trait Validable: Send + Sync + Default + PartialEq + Clone + ToString + 'static { + /// Attempts to validate and convert a string into this type. + /// + /// # Arguments + /// + /// * `text` - The input string to validate and convert. + /// + /// # Returns + /// + /// * `Ok(Self)` if the input is valid and can be converted to this type. + /// * `Err(String)` with an error message if the input is invalid. + fn validate(text: &str) -> Result; +} + +impl Validable for String { + fn validate(text: &str) -> Result { + Ok(text.to_string()) + } +} + +/// The current state of the text field validation +#[derive(Default, Clone, Debug)] +pub enum ValidationState { + /// No validation has been performed yet + #[default] + Unchecked, + /// The field content is valid + Valid, + /// The field content is invalid + Invalid(String), +} + +/// Event that is emitted when the validation state changes +#[derive(Event)] +pub struct ValidationChanged(pub ValidationState); + +/// Event that is emitted when the value changes +#[derive(Event)] +pub struct ValueChanged(pub T); + +/// This event is used to set the value of the validated input field. +#[derive(Event)] +pub struct SetValue(pub T); + +fn on_text_changed( + mut trigger: Trigger, + mut commands: Commands, + mut q_validated_input_fields: Query<&mut InputField>, +) { + let entity = trigger.entity(); + let Ok(mut field) = q_validated_input_fields.get_mut(entity) else { + return; + }; + + let new_text = trigger.new_text.clone(); + trigger.propagate(false); + + match T::validate(&new_text) { + Ok(value) => { + commands.trigger_targets(ValueChanged(value.clone()), entity); + commands.trigger_targets(ValidationChanged(ValidationState::Valid), entity); + // As editable label is controlled, we need to set the text manually + commands.trigger_targets(SetText(new_text), entity); + // Update the value only if the field is not controlled + if !field.controlled { + // Update the text in the EditableTextLine + field.old_value = value.clone(); // We need to save the old value too for field change detection + field.value = value; + } + } + Err(error) => { + // As editable label is controlled, we need to set the text manually + commands.trigger_targets(SetText(new_text), entity); + commands.trigger_targets(ValidationChanged(ValidationState::Invalid(error)), entity); + } + } +} + +fn on_value_changed( + mut commands: Commands, + mut q_changed_inputs: Query<(Entity, &mut InputField), Changed>>, +) { + for (entity, mut field) in q_changed_inputs.iter_mut() { + if field.value != field.old_value { + // info!("Trigger value rerender by field change for {:?}", entity); + field.old_value = field.value.clone(); + + // We will not trigger ValueChanged because it must be triggered only by input change + // If value field was changed by external code, we will not trigger it again + commands.trigger_targets(SetText(field.value.to_string()), entity); + commands.trigger_targets(ValidationChanged(ValidationState::Valid), entity); + } + } +} + +fn on_created( + mut commands: Commands, + q_created_inputs: Query<(Entity, &InputField), Added>>, +) { + for (entity, field) in q_created_inputs.iter() { + // Set start state + commands.trigger_targets(SetText(field.value.to_string()), entity); + commands.trigger_targets(ValidationChanged(ValidationState::Valid), entity); + } +} + +macro_rules! impl_validable_for_numeric { + ($($t:ty),*) => { + $( + impl Validable for $t { + fn validate(text: &str) -> Result { + text.parse().map_err(|_| format!("Invalid {} number", stringify!($t))) + } + } + )* + }; +} + +impl_validable_for_numeric!(i8, i16, i32, i64, i128, u8, u16, u32, u64, u128, f32, f64); diff --git a/bevy_widgets/bevy_field_forms/src/lib.rs b/bevy_widgets/bevy_field_forms/src/lib.rs new file mode 100644 index 00000000..0a71ba25 --- /dev/null +++ b/bevy_widgets/bevy_field_forms/src/lib.rs @@ -0,0 +1,61 @@ +//! This crate provides a set of widgets, which are used text input + +pub mod drag_input; +pub mod input_field; +pub mod text_event_mirror; +pub mod validate_highlight; + +use bevy::prelude::*; +use bevy_text_editing::*; + +/// Plugin for input field forms +pub struct FieldFormsPlugin; + +impl Plugin for FieldFormsPlugin { + fn build(&self, app: &mut App) { + if !app.is_plugin_added::() { + app.add_plugins(EditableTextLinePlugin); + } + + app.add_plugins(input_field::InputFieldPlugin::::default()); + app.add_plugins(drag_input::DragInputPlugin::::default()); + + app.add_plugins(input_field::InputFieldPlugin::::default()); + app.add_plugins(drag_input::DragInputPlugin::::default()); + + app.add_plugins(input_field::InputFieldPlugin::::default()); + app.add_plugins(drag_input::DragInputPlugin::::default()); + + app.add_plugins(input_field::InputFieldPlugin::::default()); + app.add_plugins(drag_input::DragInputPlugin::::default()); + + app.add_plugins(input_field::InputFieldPlugin::::default()); + app.add_plugins(drag_input::DragInputPlugin::::default()); + + app.add_plugins(input_field::InputFieldPlugin::::default()); + app.add_plugins(drag_input::DragInputPlugin::::default()); + + app.add_plugins(input_field::InputFieldPlugin::::default()); + app.add_plugins(drag_input::DragInputPlugin::::default()); + + app.add_plugins(input_field::InputFieldPlugin::::default()); + app.add_plugins(drag_input::DragInputPlugin::::default()); + + app.add_plugins(input_field::InputFieldPlugin::::default()); + app.add_plugins(drag_input::DragInputPlugin::::default()); + + app.add_plugins(input_field::InputFieldPlugin::::default()); + app.add_plugins(drag_input::DragInputPlugin::::default()); + + app.add_plugins(input_field::InputFieldPlugin::::default()); + app.add_plugins(drag_input::DragInputPlugin::::default()); + + app.add_plugins(input_field::InputFieldPlugin::::default()); + app.add_plugins(drag_input::DragInputPlugin::::default()); + + app.add_plugins(input_field::InputFieldPlugin::::default()); + + app.add_plugins(text_event_mirror::TextEventMirrorPlugin); + app.add_plugins(validate_highlight::SimpleBorderHighlightPlugin); + } +} diff --git a/bevy_widgets/bevy_field_forms/src/text_event_mirror.rs b/bevy_widgets/bevy_field_forms/src/text_event_mirror.rs new file mode 100644 index 00000000..8417920a --- /dev/null +++ b/bevy_widgets/bevy_field_forms/src/text_event_mirror.rs @@ -0,0 +1,38 @@ +//! This module provides a system to mirror the `TextChanged` event into a `SetText` event. +//! This is useful to create controlled text widgets with filters on top of this text widget. + +use bevy::prelude::*; +use bevy_text_editing::{child_traversal::FirstChildTraversal, SetText, TextChanged}; + +/// Plugin for the text event mirror. +pub struct TextEventMirrorPlugin; + +impl Plugin for TextEventMirrorPlugin { + fn build(&self, app: &mut App) { + app.add_observer(on_text_changed); + } +} + +/// Component to be added to the entity that should mirror the text event. +#[derive(Component)] +#[require(Node, FirstChildTraversal)] +pub struct TextEventMirror; + +/// Mirror propagating TextChanged event into SetText down to the text field. +/// Allow to easy create controlled text widgets with filters on top of this text widget. +fn on_text_changed( + mut trigger: Trigger, + mut commands: Commands, + q_mirrors: Query>, +) { + let entity = trigger.entity(); + let Ok(_) = q_mirrors.get(entity) else { + return; + }; + + trigger.propagate(false); + + info!("Text mirrored with value {:?}", trigger.new_text); + + commands.trigger_targets(SetText(trigger.new_text.clone()), entity); +} diff --git a/bevy_widgets/bevy_field_forms/src/validate_highlight.rs b/bevy_widgets/bevy_field_forms/src/validate_highlight.rs new file mode 100644 index 00000000..2378bd02 --- /dev/null +++ b/bevy_widgets/bevy_field_forms/src/validate_highlight.rs @@ -0,0 +1,145 @@ +//! This module provides a simple border highlight for input fields + +use crate::input_field::*; +use bevy::prelude::*; +use bevy_focus::{Focus, LostFocus}; + +/// A plugin that adds an observer to highlight the border of a text field based on its validation state based on the `SimpleBorderHighlight` component. +pub struct SimpleBorderHighlightPlugin; + +impl Plugin for SimpleBorderHighlightPlugin { + fn build(&self, app: &mut App) { + app.add_observer(on_validation_changed); + app.add_observer(on_focus_added); + app.add_observer(on_focus_lost); + + app.add_systems(PreUpdate, on_interaction_changed); + } +} + +/// A component that defines colors for highlighting the border of a text field based on its validation state. +#[derive(Component, Clone)] +#[require(Node, Interaction)] +pub struct SimpleBorderHighlight { + /// The color of the border when the text field's content is valid. + pub normal_color: Color, + /// The color of the border when the text field is hovered. + pub hovered_color: Color, + /// The color of the border when the text field is in focus. + pub focused_color: Color, + /// The color of the border when the text field's content is invalid. + pub invalid_color: Color, + /// The last validation state of the text field. + pub last_validation_state: ValidationState, +} + +impl Default for SimpleBorderHighlight { + fn default() -> Self { + Self { + normal_color: Color::srgb(0.5, 0.5, 0.5), + hovered_color: Color::srgb(0.7, 0.7, 0.7), + focused_color: Color::srgb(1.0, 1.0, 1.0), + invalid_color: Color::srgb(1.0, 0.0, 0.0), + last_validation_state: ValidationState::Unchecked, + } + } +} + +fn on_validation_changed( + trigger: Trigger, + mut commands: Commands, + mut q_highlights: Query<(&mut SimpleBorderHighlight, &Interaction, Option<&Focus>)>, +) { + let entity = trigger.entity(); + let Ok((mut highlight, interaction, focus)) = q_highlights.get_mut(entity) else { + return; + }; + + match &trigger.0 { + ValidationState::Valid => { + if focus.is_some() { + commands + .entity(entity) + .insert(BorderColor(highlight.focused_color)); + } else if *interaction == Interaction::Hovered { + commands + .entity(entity) + .insert(BorderColor(highlight.hovered_color)); + } else { + commands + .entity(entity) + .insert(BorderColor(highlight.normal_color)); + } + } + ValidationState::Invalid(_) => { + commands + .entity(entity) + .insert(BorderColor(highlight.invalid_color)); + } + ValidationState::Unchecked => { + if focus.is_some() { + commands + .entity(entity) + .insert(BorderColor(highlight.focused_color)); + } else if *interaction == Interaction::Hovered { + commands + .entity(entity) + .insert(BorderColor(highlight.hovered_color)); + } else { + commands + .entity(entity) + .insert(BorderColor(highlight.normal_color)); + } + } + } + + highlight.last_validation_state = trigger.0.clone(); +} + +fn on_focus_added( + trigger: Trigger, + mut commands: Commands, + q_highlights: Query<&SimpleBorderHighlight>, +) { + let entity = trigger.entity(); + let Ok(highlight) = q_highlights.get(entity) else { + return; + }; + + info!("Focus added to {:?}", entity); + + commands.trigger_targets( + ValidationChanged(highlight.last_validation_state.clone()), + entity, + ); +} + +fn on_focus_lost( + trigger: Trigger, + mut commands: Commands, + q_highlights: Query<&SimpleBorderHighlight>, +) { + let entity = trigger.entity(); + let Ok(highlight) = q_highlights.get(entity) else { + return; + }; + + info!("Focus lost from {:?}", entity); + + commands.trigger_targets( + ValidationChanged(highlight.last_validation_state.clone()), + entity, + ); +} + +fn on_interaction_changed( + mut commands: Commands, + q_changed_interaction: Query<(Entity, &SimpleBorderHighlight), Changed>, +) { + for (entity, highlight) in q_changed_interaction.iter() { + commands.trigger_targets( + ValidationChanged(highlight.last_validation_state.clone()), + entity, + ); + } +} diff --git a/bevy_widgets/bevy_focus/Cargo.toml b/bevy_widgets/bevy_focus/Cargo.toml new file mode 100644 index 00000000..f47996d6 --- /dev/null +++ b/bevy_widgets/bevy_focus/Cargo.toml @@ -0,0 +1,10 @@ +[package] +name = "bevy_focus" +version = "0.1.0" +edition = "2021" + +[dependencies] +bevy.workspace = true + +[lints] +workspace = true diff --git a/bevy_widgets/bevy_focus/src/lib.rs b/bevy_widgets/bevy_focus/src/lib.rs new file mode 100644 index 00000000..44e79423 --- /dev/null +++ b/bevy_widgets/bevy_focus/src/lib.rs @@ -0,0 +1,134 @@ +//! This crate contains input focus management for Bevy widgets. +//! Currently only one entity can hold focus at a time. + +use bevy::prelude::*; + +/// Plugin for input focus logic +pub struct FocusPlugin; + +impl Plugin for FocusPlugin { + fn build(&self, app: &mut App) { + app.add_event::(); + app.add_event::(); + app.add_event::(); + app.add_event::(); + + app.add_observer(set_focus); + app.add_observer(clear_focus); + app.add_observer(mouse_click); + + app.add_systems( + Last, + clear_focus_after_click.run_if(resource_exists::), + ); + } +} + +/// Component which indicates that a widget is focused and can receive input events. +#[derive(Component, Debug)] +pub struct Focus; + +/// Mark that a widget can receive input events and can be focused +#[derive(Component, Default)] +pub struct Focusable; + +/// Event indicating that a widget has received focus +#[derive(Event)] +pub struct GotFocus(pub Option>); + +/// Event indicating that a widget has lost focus +#[derive(Event)] +pub struct LostFocus; + +/// Set focus to a widget +#[derive(Event)] +pub struct SetFocus; + +/// Clear focus from widgets +#[derive(Event)] +pub struct ClearFocus; + +/// Extension trait for [`Commands`] +/// Contains commands to set and clear input focus +pub trait FocusExt { + /// Set input focus to the given targets + fn set_focus(&mut self, target: Entity); + + /// Clear input focus + fn clear_focus(&mut self); +} + +impl FocusExt for Commands<'_, '_> { + fn set_focus(&mut self, target: Entity) { + self.trigger_targets(SetFocus, target); + } + + fn clear_focus(&mut self) { + self.trigger(ClearFocus); + } +} + +#[derive(Resource)] +struct NeedClearFocus(bool); + +fn set_focus( + trigger: Trigger, + mut commands: Commands, + q_focused: Query>, +) { + for entity in q_focused.iter() { + if entity == trigger.entity() { + continue; + } + commands.entity(entity).remove::(); + commands.trigger_targets(LostFocus, entity); + } + commands.entity(trigger.entity()).insert(Focus); + commands.trigger_targets(GotFocus(None), trigger.entity()); +} + +fn clear_focus( + _: Trigger, + mut commands: Commands, + q_focused: Query>, +) { + for entity in q_focused.iter() { + commands.entity(entity).remove::(); + commands.trigger_targets(LostFocus, entity); + } +} + +fn mouse_click( + mut click: Trigger>, + mut commands: Commands, + q_focusable: Query>, + q_focused: Query>, +) { + if click.event().button != PointerButton::Primary { + return; + } + let entity = click.entity(); + if q_focusable.contains(entity) { + commands.insert_resource(NeedClearFocus(false)); + + click.propagate(false); + for e in q_focused.iter() { + if e == entity { + continue; + } + commands.entity(e).remove::(); + commands.trigger_targets(LostFocus, e); + } + commands.entity(entity).insert(Focus); + commands.trigger_targets(GotFocus(Some(click.event().clone())), entity); + } else { + commands.insert_resource(NeedClearFocus(true)); + } +} + +fn clear_focus_after_click(mut commands: Commands, need_clear_focus: Res) { + if need_clear_focus.0 { + commands.clear_focus(); + commands.remove_resource::(); + } +} diff --git a/bevy_widgets/bevy_incomplete_bsn/Cargo.toml b/bevy_widgets/bevy_incomplete_bsn/Cargo.toml new file mode 100644 index 00000000..6695b907 --- /dev/null +++ b/bevy_widgets/bevy_incomplete_bsn/Cargo.toml @@ -0,0 +1,10 @@ +[package] +name = "bevy_incomplete_bsn" +version = "0.1.0" +edition = "2021" + +[dependencies] +bevy.workspace = true + +[lints] +workspace = true diff --git a/bevy_widgets/bevy_incomplete_bsn/src/children_patcher.rs b/bevy_widgets/bevy_incomplete_bsn/src/children_patcher.rs new file mode 100644 index 00000000..fb916c71 --- /dev/null +++ b/bevy_widgets/bevy_incomplete_bsn/src/children_patcher.rs @@ -0,0 +1,27 @@ +use std::sync::Arc; + +use bevy::{prelude::*, reflect::FromType}; + +use crate::entity_diff_tree::DiffTree; + +/// A mark component that want to transform diff tree +/// For example, collapsing header want to wrap all children in a collapsable node +pub trait ChildrenPatcher: Send + Sync + 'static { + fn children_patch(&mut self, children: &mut Vec); +} + +#[derive(Clone)] +pub struct ReflectChildrenPatcher { + pub func: Arc) + Send + Sync + 'static>, +} + +impl FromType for ReflectChildrenPatcher { + fn from_type() -> Self { + Self { + func: Arc::new(move |reflect, children| { + let typed = reflect.downcast_mut::().unwrap(); + typed.children_patch(children) + }), + } + } +} diff --git a/bevy_widgets/bevy_incomplete_bsn/src/construct.rs b/bevy_widgets/bevy_incomplete_bsn/src/construct.rs new file mode 100644 index 00000000..3721fdb1 --- /dev/null +++ b/bevy_widgets/bevy_incomplete_bsn/src/construct.rs @@ -0,0 +1,107 @@ +//! This module provides the construct trait, which is used to create widgets + +use bevy::prelude::*; + +use crate::{construct_context::ConstructContext, construct_patch::ConstructPatch}; + +pub trait Construct: Sized { + type Props: Default + Clone; + fn construct( + context: &mut ConstructContext, + props: Self::Props, + ) -> Result; + + fn patch( + func: impl FnMut(&mut Self::Props), + ) -> ConstructPatch { + ConstructPatch::new(func) + } +} + +#[derive(Debug, Clone, PartialEq, Eq, Hash, Deref, DerefMut)] +pub struct ConstructError(pub String); + +impl Construct for T { + type Props = T; + + #[inline] + fn construct( + _context: &mut ConstructContext, + props: Self::Props, + ) -> Result { + Ok(props) + } +} + +#[cfg(test)] +mod tests { + use crate::patch::Patch; + + use super::*; + + #[derive(Default, Clone, Component)] + struct TestProps { + value: i32, + } + + struct TestConstruct; + + impl Construct for TestConstruct { + type Props = TestProps; + + fn construct( + context: &mut ConstructContext, + props: Self::Props, + ) -> Result { + context.world.entity_mut(context.id).insert(props.clone()); + Ok(TestConstruct) + } + } + + #[test] + fn test_construct_trait() { + let mut world = World::default(); + let entity = world.spawn_empty().id(); + let mut context = ConstructContext { + id: entity, + world: &mut world, + }; + + let props = TestProps { value: 42 }; + let result = TestConstruct::construct(&mut context, props); + + assert!(result.is_ok()); + assert_eq!( + world.entity(entity).get::().map(|p| p.value), + Some(42) + ); + } + + #[test] + fn test_default_construct_implementation() { + let mut world = World::default(); + let entity = world.spawn_empty().id(); + let mut context = ConstructContext { + id: entity, + world: &mut world, + }; + + let props = 123; + let result = i32::construct(&mut context, props); + + assert!(result.is_ok()); + assert_eq!(result.unwrap(), 123); + } + + #[test] + fn test_construct_transform() { + let mut world = World::default(); + + let mut transform = Transform::default(); + let mut patch = Transform::patch(|props| { + props.translation.x = 1.0; + }); + patch.patch(&mut transform); + assert_eq!(transform.translation.x, 1.0); + } +} diff --git a/bevy_widgets/bevy_incomplete_bsn/src/construct_context.rs b/bevy_widgets/bevy_incomplete_bsn/src/construct_context.rs new file mode 100644 index 00000000..d0241625 --- /dev/null +++ b/bevy_widgets/bevy_incomplete_bsn/src/construct_context.rs @@ -0,0 +1,8 @@ +//! This module provides the context for the construct trait + +use bevy::prelude::*; + +pub struct ConstructContext<'a> { + pub id: Entity, + pub world: &'a mut World, +} diff --git a/bevy_widgets/bevy_incomplete_bsn/src/construct_patch.rs b/bevy_widgets/bevy_incomplete_bsn/src/construct_patch.rs new file mode 100644 index 00000000..e1367da0 --- /dev/null +++ b/bevy_widgets/bevy_incomplete_bsn/src/construct_patch.rs @@ -0,0 +1,33 @@ +//! Construct patch is a auto generated patch for every type that implements the Construct trait + +use bevy::prelude::*; + +use crate::{construct::Construct, patch::Patch}; + +pub struct ConstructPatch { + pub func: F, + _marker: std::marker::PhantomData, +} + +impl ConstructPatch +where + T: Construct, +{ + pub fn new(func: F) -> Self { + Self { + func, + _marker: std::marker::PhantomData, + } + } +} + +impl Patch for ConstructPatch +where + T: Construct + Bundle + Default + Clone, + F: FnMut(&mut T::Props) + Send + Sync + 'static, +{ + type Construct = T; + fn patch(&mut self, props: &mut T::Props) { + (self.func)(props); + } +} diff --git a/bevy_widgets/bevy_incomplete_bsn/src/entity_diff_tree.rs b/bevy_widgets/bevy_incomplete_bsn/src/entity_diff_tree.rs new file mode 100644 index 00000000..e57f4e7e --- /dev/null +++ b/bevy_widgets/bevy_incomplete_bsn/src/entity_diff_tree.rs @@ -0,0 +1,405 @@ +//! Allow to automatically manage created components and children in tree by storing last minimal tree state + +use std::any::{Any, TypeId}; + +use bevy::{ + ecs::{component::ComponentId, system::IntoObserverSystem}, + prelude::*, + reflect::{FromType, ReflectFromPtr, Type}, + utils::HashSet, +}; + +use crate::{children_patcher::ReflectChildrenPatcher, construct::Construct, patch::Patch}; + +/// Represents a tree structure for managing entity differences and patches. +/// +/// This structure is designed to hold a collection of patches that can be applied to an entity, +/// as well as a list of child trees that represent the entity's children in the scene hierarchy. +#[derive(Default)] +pub struct DiffTree { + /// A vector of patches that can be applied to the entity to modify its components. + /// Each patch is a boxed trait object that implements `EntityComponentDiffPatch`. + pub patch: Vec>, + /// A vector of patches that are creating observers + pub observer_patch: Vec>, + /// A vector of child trees, each representing a child entity in the scene hierarchy. + pub children: Vec, +} + +impl DiffTree { + /// Creates a new `EntityDiffTree` with empty patch and children. + pub fn new() -> Self { + Self { + patch: Vec::new(), + observer_patch: Vec::new(), + children: Vec::new(), + } + } + + pub fn add_patch(&mut self, patch: impl EntityComponentDiffPatch) { + self.patch.push(Box::new(patch)); + } + + pub fn add_patch_fn( + &mut self, + func: impl FnMut(&mut C) + Send + Sync + 'static, + ) { + self.add_patch(::patch(func)); + } + + pub fn add_child(&mut self, child: DiffTree) { + self.children.push(child); + } + + /// Adds a patch to the entity. + pub fn with_patch(mut self, patch: impl EntityComponentDiffPatch) -> Self { + self.patch.push(Box::new(patch)); + self + } + + /// Adds a patch to the entity that is a function that mutate a component. + pub fn with_patch_fn( + mut self, + func: impl FnMut(&mut C) + Send + Sync + 'static, + ) -> Self { + self.with_patch(::patch(func)) + } + + /// Adds a child to the entity. + pub fn with_child(mut self, child: DiffTree) -> Self { + self.children.push(child); + self + } + + /// Applies the patch to the entity and its children. + pub fn apply(&mut self, entity: Entity, world: &mut World) { + let mut new_component_set = HashSet::new(); + { + let mut entity_mut = world.entity_mut(entity); + for patch in self.patch.iter_mut() { + patch.entity_patch(&mut entity_mut); + // SAFETY: we are not mutate any component or entity. Only read component id from components and register it if not registered yet + #[allow(unsafe_code)] + unsafe { + new_component_set.insert(patch.component_id(entity_mut.world_mut())); + } + } + + if let Some(last_state) = entity_mut.get::().cloned() { + // Remove all components that was used in previous tree state but not in current + for c_id in last_state + .component_ids + .iter() + .filter(|c_id| !new_component_set.contains(*c_id)) + { + entity_mut.remove_by_id(*c_id); + info!("Removed component {:?}", c_id); + } + } + } + + // Apply children patches + { + world.resource_scope(|world, app_registry: Mut| { + let type_registry = app_registry.read(); + let mut entity_mut = world.entity_mut(entity); + let Self { + patch, + observer_patch, + children, + } = self; + for patch in patch.iter() { + #[allow(unsafe_code)] + let c_id = unsafe { patch.component_id(entity_mut.world_mut()) }; + + let Some(data) = + type_registry.get_type_data::(patch.get_type_id()) + else { + continue; + }; + + let Some(reflect_from_ptr) = + type_registry.get_type_data::(patch.get_type_id()) + else { + continue; + }; + + let Ok(mut ptr) = entity_mut.get_mut_by_id(c_id) else { + continue; + }; + + #[allow(unsafe_code)] + let reflect = unsafe { reflect_from_ptr.as_reflect_mut(ptr.as_mut()) }; + + (data.func)(reflect, children); + } + }); + } + + // Clear all observers that was used in previous tree state + if let Some(last_state) = world.entity(entity).get::().cloned() { + for observer in last_state.observers.iter() { + world.entity_mut(*observer).despawn_recursive(); + } + } + + // Take vector of observers patches and clear it + let mut new_observers = self + .observer_patch + .drain(..) + .into_iter() + .map(|mut patch| patch.as_mut().observer_patch(entity, world)) + .collect::>(); + + // We will use separate "children" vector to avoid conflicts with inner logic of widgets which also can use children (For example InputField spawn children for self) + let mut children_entities = + if let Some(last_state) = world.entity(entity).get::().cloned() { + last_state.children + } else { + Vec::new() + }; + + while children_entities.len() < self.children.len() { + let child_entity = world.spawn_empty().id(); + world.entity_mut(entity).add_child(child_entity); + children_entities.push(child_entity); + } + + for (i, child) in self.children.iter_mut().enumerate() { + child.apply(children_entities[i], world); + } + + // Clear unused children + for i in self.children.len()..children_entities.len() { + world.entity_mut(children_entities[i]).despawn_recursive(); + info!("Despawned child {:?}", children_entities[i]); + } + + // Store current state + world.entity_mut(entity).insert(LastTreeState { + component_ids: new_component_set, + children: children_entities, + observers: new_observers, + }); + } + + fn contains_component(&self) -> bool { + self.patch + .iter() + .any(|patch| patch.get_type_id() == TypeId::of::()) + } + + pub fn add_cascade_patch_fn( + &mut self, + func: impl Fn(&mut C) + Send + Sync + 'static + Clone, + ) { + if self.contains_component::() { + self.add_patch_fn(func.clone()); + } else { + for child in self.children.iter_mut() { + child.add_cascade_patch_fn::(func.clone()); + } + } + } + + pub fn add_observer_patch( + &mut self, + func: impl IntoObserverSystem + Send + Sync + 'static, + ) { + self.observer_patch.push(Box::new(FnObserverPatch { + func: Some(Box::new(move |entity, world| { + let mut observer = Observer::new(func); + observer.watch_entity(entity); + world.spawn(observer).id() + })), + })); + } +} + +pub trait ObserverPatch: Send + Sync + 'static { + fn observer_patch(&mut self, entity: Entity, world: &mut World) -> Entity; +} + +struct FnObserverPatch { + func: Option Entity + Send + Sync + 'static>>, +} + +impl ObserverPatch for FnObserverPatch { + fn observer_patch(&mut self, entity: Entity, world: &mut World) -> Entity { + self.func.take().unwrap()(entity, world) + } +} + +/// This trait is used to modify an entity's components and store the component's ID for tracking purposes. +pub trait EntityComponentDiffPatch: Send + Sync + 'static { + /// Applies the patch to the given entity. + fn entity_patch(&mut self, entity_mut: &mut EntityWorldMut); + + /// Returns the ComponentId of the component that this patch is associated with. + /// This is used to keep track of the components that were present in an entity during the last update. + fn component_id(&self, world: &mut World) -> ComponentId; + + /// Returns the TypeId of the component that this patch is associated with. + fn get_type_id(&self) -> TypeId; +} + +impl> EntityComponentDiffPatch for T { + fn entity_patch(&mut self, entity_mut: &mut EntityWorldMut) { + if !entity_mut.contains::() { + entity_mut.insert(C::default()); + } + + let mut component = entity_mut.get_mut::().unwrap(); + self.patch(&mut component); + } + + fn component_id(&self, world: &mut World) -> ComponentId { + if let Some(c_id) = world.components().component_id::() { + c_id + } else { + world.register_component::() + } + } + + fn get_type_id(&self) -> TypeId { + TypeId::of::() + } +} + +/// Represents the state of an entity's component tree from the last update. +/// +/// This struct is used to keep track of the components and children that were +/// present in an entity during the last tree update. It helps in efficiently +/// determining what has changed in subsequent updates. +#[derive(Default, Component, Clone)] +pub struct LastTreeState { + /// A set of ComponentIds representing the components that were present + /// in the entity during the last update. + pub component_ids: HashSet, + + /// The used child entities that the entity had during the last update. + pub children: Vec, + + /// A vector of observers that were created during the last update. + pub observers: Vec, +} + +/// A trait for applying an `EntityDiffTree` to an entity using `EntityCommands`. +/// +/// This trait extends the functionality of `EntityCommands` to allow for +/// the application of an `EntityDiffTree`, which represents a set of changes +/// to be applied to an entity's components and children. +pub trait DiffTreeCommands { + /// Applies the given `EntityDiffTree` to the entity. + /// + /// This method queues a command that will apply all the changes + /// specified in the `EntityDiffTree` to the entity when the command + /// is executed. + /// + /// # Arguments + /// + /// * `tree` - The `EntityDiffTree` containing the changes to apply to the entity. + fn diff_tree(&mut self, tree: DiffTree); +} + +impl DiffTreeCommands for EntityCommands<'_> { + fn diff_tree(&mut self, mut tree: DiffTree) { + self.queue(move |entity: Entity, world: &mut World| { + tree.apply(entity, world); + }); + } +} + +#[cfg(test)] +mod tests { + use crate::construct::Construct; + + use super::*; + use bevy::prelude::*; + + #[test] + fn create_default_component() { + let mut world = World::default(); + let entity = world.spawn_empty().id(); + let mut tree = DiffTree::new().with_patch(Transform::patch(|transform| { + transform.translation = Vec3::new(1.0, 2.0, 3.0) + })); + + tree.apply(entity, &mut world); + + let transform = world.entity(entity).get::().unwrap(); + // Check that patch was applied + assert_eq!(transform.translation, Vec3::new(1.0, 2.0, 3.0)); + // Check that other fields are default + assert_eq!(transform.rotation, Quat::IDENTITY); + assert_eq!(transform.scale, Vec3::ONE); + } + + #[test] + fn check_component_removal() { + let mut world = World::default(); + let entity = world.spawn_empty().id(); + let mut tree = DiffTree::new().with_patch(Transform::patch(|transform| { + transform.translation = Vec3::new(1.0, 2.0, 3.0) + })); + + tree.apply(entity, &mut world); + + assert!(world.entity(entity).contains::()); + + let mut second_tree = DiffTree::new().with_patch(Name::patch(|name| { + name.set("test"); + })); + + second_tree.apply(entity, &mut world); + + assert!(world.entity(entity).contains::()); + assert!(!world.entity(entity).contains::()); + } + + #[test] + fn check_children_create_and_remove() { + let mut world = World::default(); + let entity = world.spawn_empty().id(); + let mut tree = DiffTree::new() + .with_patch(Transform::patch(|transform| { + transform.translation = Vec3::new(1.0, 2.0, 3.0) + })) + .with_child(DiffTree::new().with_patch(Transform::patch(|t| { + t.translation = Vec3::new(4.0, 5.0, 6.0) + }))); + + tree.apply(entity, &mut world); + + assert_eq!(world.entity(entity).get::().unwrap().len(), 1); + let child_entity = world.entity(entity).get::().unwrap()[0]; + assert_eq!( + world + .entity(child_entity) + .get::() + .unwrap() + .translation, + Vec3::new(4.0, 5.0, 6.0) + ); + + let mut second_tree = DiffTree::new(); + second_tree.apply(entity, &mut world); + + assert!(world.get_entity(child_entity).is_err()); + assert_eq!(world.entity(entity).get::().unwrap().len(), 0); + } + + #[test] + fn test_fn_patches() { + let mut world = World::default(); + let entity = world.spawn_empty().id(); + let mut tree = DiffTree::new().with_patch_fn(|t: &mut Transform| { + t.translation = Vec3::new(1.0, 2.0, 3.0); + }); + + tree.apply(entity, &mut world); + + let transform = world.entity(entity).get::().unwrap(); + assert_eq!(transform.translation, Vec3::new(1.0, 2.0, 3.0)); + } +} diff --git a/bevy_widgets/bevy_incomplete_bsn/src/entity_patch.rs b/bevy_widgets/bevy_incomplete_bsn/src/entity_patch.rs new file mode 100644 index 00000000..e458aa90 --- /dev/null +++ b/bevy_widgets/bevy_incomplete_bsn/src/entity_patch.rs @@ -0,0 +1,67 @@ +//! Entity patch is a patch that is used to update the entity tree + +use std::sync::Arc; + +use bevy::prelude::*; + +use crate::{construct::Construct, patch::Patch}; + +#[derive(Default)] +pub struct EntityPatch { + pub patch: Vec>, + pub children: Vec, +} + +impl EntityPatch { + pub fn new() -> Self { + Self { + patch: Vec::new(), + children: Vec::new(), + } + } + + pub fn with_patch(mut self, patch: impl EntityComponentPatch) -> Self { + self.patch.push(Box::new(patch)); + self + } + + pub fn with_child(mut self, child: EntityPatch) -> Self { + self.children.push(child); + self + } + + pub fn apply(&mut self, entity: Entity, world: &mut World) { + { + let mut entity_mut = world.entity_mut(entity); + for patch in self.patch.iter_mut() { + patch.entity_patch(&mut entity_mut); + } + } + + let mut children_entities = Vec::new(); + if let Some(children) = world.entity(entity).get::() { + children_entities = children.iter().map(|e| *e).collect(); + } + + while children_entities.len() < self.children.len() { + let child_entity = world.spawn_empty().id(); + world.entity_mut(entity).add_child(child_entity); + children_entities.push(child_entity); + } + + for (i, child) in self.children.iter_mut().enumerate() { + child.apply(children_entities[i], world); + } + } +} + +pub trait EntityComponentPatch: Send + Sync + 'static { + fn entity_patch(&mut self, entity_mut: &mut EntityWorldMut); +} + +impl> EntityComponentPatch for T { + fn entity_patch(&mut self, entity_mut: &mut EntityWorldMut) { + let mut component = entity_mut.get_mut::().unwrap(); + self.patch(&mut component); + } +} diff --git a/bevy_widgets/bevy_incomplete_bsn/src/lib.rs b/bevy_widgets/bevy_incomplete_bsn/src/lib.rs new file mode 100644 index 00000000..61165af4 --- /dev/null +++ b/bevy_widgets/bevy_incomplete_bsn/src/lib.rs @@ -0,0 +1,9 @@ +//! This crate provides a partial implementation of the BSN design + +pub mod children_patcher; +pub mod construct; +pub mod construct_context; +pub mod construct_patch; +pub mod entity_diff_tree; +pub mod entity_patch; +pub mod patch; diff --git a/bevy_widgets/bevy_incomplete_bsn/src/patch.rs b/bevy_widgets/bevy_incomplete_bsn/src/patch.rs new file mode 100644 index 00000000..09d3933c --- /dev/null +++ b/bevy_widgets/bevy_incomplete_bsn/src/patch.rs @@ -0,0 +1,37 @@ +//! This module provides the patch trait, which is used to update widget tree + +use crate::construct::Construct; +use bevy::prelude::*; + +pub trait Patch: Send + Sync + 'static { + type Construct: Construct + Bundle + Default + Clone; + + fn patch(&mut self, props: &mut <::Construct as Construct>::Props); +} + +#[cfg(test)] +mod tests { + use super::*; + + #[derive(Default, Clone, Component)] + struct TestProps { + value: i32, + } + + struct TestPatch; + + impl Patch for TestPatch { + type Construct = TestProps; + fn patch(&mut self, props: &mut TestProps) { + props.value += 1; + } + } + + #[test] + fn test_patch_trait() { + let mut props = TestProps { value: 0 }; + let mut patch = TestPatch; + patch.patch(&mut props); + assert_eq!(props.value, 1); + } +} diff --git a/bevy_widgets/bevy_text_editing/Cargo.toml b/bevy_widgets/bevy_text_editing/Cargo.toml new file mode 100644 index 00000000..0fc9b5ca --- /dev/null +++ b/bevy_widgets/bevy_text_editing/Cargo.toml @@ -0,0 +1,13 @@ +[package] +name = "bevy_text_editing" +version = "0.1.0" +edition = "2021" + +[dependencies] +bevy.workspace = true +bevy_focus.workspace = true +bevy_clipboard.workspace = true +bevy_i-cant-believe-its-not-bsn.workspace = true + +[lints] +workspace = true diff --git a/bevy_widgets/bevy_text_editing/examples/editable_label.rs b/bevy_widgets/bevy_text_editing/examples/editable_label.rs new file mode 100644 index 00000000..3795493d --- /dev/null +++ b/bevy_widgets/bevy_text_editing/examples/editable_label.rs @@ -0,0 +1,38 @@ +//! This example shows how to create editable label with bevy_text_editing + +use bevy::prelude::*; +use bevy_text_editing::editable_text_line::*; + +fn main() { + App::new() + .add_plugins(DefaultPlugins) + .add_plugins(EditableTextLinePlugin) + .add_systems(Startup, setup) + .run(); +} + +fn setup(mut commands: Commands) { + commands.spawn(Camera2d::default()); + + commands + .spawn(Node { + width: Val::Percent(100.), + height: Val::Percent(100.), + justify_content: JustifyContent::Center, + align_items: AlignItems::Center, + ..Default::default() + }) + .with_children(|cmd| { + cmd.spawn(( + EditableTextLine::new("Hello, World!"), + Node { + // We need to manually set the width and height for the editable text line + // It is limitation of current implementation + width: Val::Px(300.0), + height: Val::Px(25.0), + ..Default::default() + }, + BackgroundColor(Color::srgba(0.0, 0.0, 0.0, 0.5)), // We can use any background color (or any borders/border color) + )); + }); +} diff --git a/bevy_widgets/bevy_text_editing/examples/password.rs b/bevy_widgets/bevy_text_editing/examples/password.rs new file mode 100644 index 00000000..990047ec --- /dev/null +++ b/bevy_widgets/bevy_text_editing/examples/password.rs @@ -0,0 +1,98 @@ +//! This example demonstrates how to create a password input field using the `EditableTextLine` component. +//! +//! The password field is created with the following features: +//! - Masked input (characters are displayed as asterisks) +//! - A custom style to visually represent a password field +//! - A `Password` component to store the actual password value +//! +//! Run this example with: +//! ``` +//! cargo run --example password +//! ``` + +use bevy::prelude::*; +use bevy_text_editing::*; + +fn main() { + App::new() + .add_plugins(DefaultPlugins) + .add_plugins(EditableTextLinePlugin) + .add_systems(Startup, setup) + .add_observer(update_password) + .add_systems(Update, show_password) + .run(); +} + +fn setup(mut commands: Commands) { + commands.spawn(Camera2d::default()); + + let show_password_id = commands.spawn(Text::new("")).id(); + + let password_id = commands + .spawn(( + Password::default(), + EditableTextLine::controlled(""), + Node { + width: Val::Px(300.0), + height: Val::Px(25.0), + ..Default::default() + }, + BackgroundColor(Color::srgb(0.5, 0.5, 0.5)), + ShowPassword(show_password_id), + )) + .id(); + + commands + .spawn(Node { + width: Val::Percent(100.0), + height: Val::Percent(100.0), + display: Display::Flex, + flex_direction: FlexDirection::Column, + justify_content: JustifyContent::Center, + align_items: AlignItems::Center, + ..Default::default() + }) + .add_child(password_id) + .with_child(Text::new("Password:")) + .add_child(show_password_id); +} + +#[derive(Component, Default)] +struct Password { + val: String, +} + +#[derive(Component)] +struct ShowPassword(pub Entity); + +fn update_password( + trigger: Trigger, + mut commands: Commands, + mut q_passwords: Query<&mut Password>, +) { + let entity = trigger.entity(); + let Ok(mut password) = q_passwords.get_mut(entity) else { + return; + }; + + info!("Text changed: {:?}", trigger.change); + + trigger.change.apply(&mut password.val); + + info!("Password: {:?}", password.val); + + let asterisks = "*".repeat(password.val.chars().count()); + commands.trigger_targets(SetText(asterisks), entity); +} + +fn show_password( + q_password_changed: Query<(&Password, &ShowPassword), Changed>, + mut q_texts: Query<&mut Text>, +) { + for (password, show_password) in q_password_changed.iter() { + let Ok(mut text) = q_texts.get_mut(show_password.0) else { + return; + }; + text.0 = password.val.clone(); + } +} diff --git a/bevy_widgets/bevy_text_editing/src/char_poistion.rs b/bevy_widgets/bevy_text_editing/src/char_poistion.rs new file mode 100644 index 00000000..df94f561 --- /dev/null +++ b/bevy_widgets/bevy_text_editing/src/char_poistion.rs @@ -0,0 +1,96 @@ +use bevy::prelude::*; + +/// A component that stores a character position in a text +/// Separated from `usize` to make it clear that it is a character position, not a byte position. +/// And prevents accidental usage as a byte position in string[..byte_position] +#[derive(Reflect, Default, Clone, Copy, Debug)] +pub struct CharPosition(pub(crate) usize); + +impl std::ops::Add for CharPosition { + type Output = Self; + + fn add(self, rhs: CharPosition) -> Self::Output { + Self(self.0 + rhs.0) + } +} + +impl std::ops::Sub for CharPosition { + type Output = Self; + + fn sub(self, rhs: CharPosition) -> Self::Output { + Self(self.0 - rhs.0) + } +} + +impl std::ops::Add for CharPosition { + type Output = Self; + + fn add(self, rhs: usize) -> Self::Output { + Self(self.0 + rhs) + } +} + +impl std::ops::Sub for CharPosition { + type Output = Self; + + fn sub(self, rhs: usize) -> Self::Output { + Self(self.0 - rhs) + } +} + +impl PartialOrd for CharPosition { + fn partial_cmp(&self, other: &Self) -> Option { + self.0.partial_cmp(&other.0) + } +} + +impl PartialEq for CharPosition { + fn eq(&self, other: &Self) -> bool { + self.0 == other.0 + } +} + +impl PartialEq for CharPosition { + fn eq(&self, other: &usize) -> bool { + self.0 == *other + } +} + +impl PartialOrd for CharPosition { + fn partial_cmp(&self, other: &usize) -> Option { + self.0.partial_cmp(other) + } +} + +impl PartialEq for usize { + fn eq(&self, other: &CharPosition) -> bool { + *self == other.0 + } +} + +impl PartialOrd for usize { + fn partial_cmp(&self, other: &CharPosition) -> Option { + self.partial_cmp(&other.0) + } +} + +impl std::fmt::Display for CharPosition { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!(f, "{}", self.0) + } +} + +impl From for CharPosition { + fn from(value: usize) -> Self { + Self(value) + } +} + +/// Get the byte position of a character placed at a given position in a string +pub fn get_byte_position(text: &str, char_position: CharPosition) -> usize { + if char_position.0 < text.chars().count() { + text.char_indices().nth(char_position.0).unwrap().0 + } else { + text.len() + } +} diff --git a/bevy_widgets/bevy_text_editing/src/child_traversal.rs b/bevy_widgets/bevy_text_editing/src/child_traversal.rs new file mode 100644 index 00000000..6e3a5cdf --- /dev/null +++ b/bevy_widgets/bevy_text_editing/src/child_traversal.rs @@ -0,0 +1,53 @@ +//! This module provides functionality for traversing events to the first child of an entity. +//! +//! It includes: +//! - `FirstChildTraversalPlugin`: A plugin that sets up the necessary systems for child traversal. +//! - `FirstChildTraversal`: A marker component for entities that should use first-child traversal. +//! - `CachedFirsChild`: A component that caches the first child of an entity for efficient traversal. +//! +//! The module also implements the `Traversal` trait for `CachedFirsChild`, allowing for easy +//! integration with Bevy's event system. + +use bevy::{ecs::traversal::Traversal, prelude::*}; + +/// Plugin for traversing events to the first child of an entity +pub struct FirstChildTraversalPlugin; + +impl Plugin for FirstChildTraversalPlugin { + fn build(&self, app: &mut App) { + app.add_systems(PreUpdate, auto_update_cache); + } +} + +/// Marker for traversing events to the first child of an entity +#[derive(Component, Debug, Default)] +pub struct FirstChildTraversal; + +/// State for caching the first child of an entity to make Traverse trait easier to implement +#[derive(Component, Debug)] +pub struct CachedFirsChild(pub Entity); + +impl Traversal for &'static CachedFirsChild { + fn traverse(item: Self::Item<'_>) -> Option { + Some(item.0) + } +} + +fn auto_update_cache( + mut commands: Commands, + q_changed_children: Query< + (Entity, &Children), + ( + With, + Or<(Changed, Added)>, + ), + >, +) { + for (entity, children) in q_changed_children.iter() { + if let Some(first_child) = children.first() { + commands + .entity(entity) + .insert(CachedFirsChild(*first_child)); + } + } +} diff --git a/bevy_widgets/bevy_text_editing/src/cursor.rs b/bevy_widgets/bevy_text_editing/src/cursor.rs new file mode 100644 index 00000000..e349b3b4 --- /dev/null +++ b/bevy_widgets/bevy_text_editing/src/cursor.rs @@ -0,0 +1,39 @@ +//! Cursor plugin for text field + +use bevy::prelude::*; + +pub(crate) struct CursorPlugin; + +impl Plugin for CursorPlugin { + fn build(&self, app: &mut App) { + app.add_systems(Update, update_cursor); + } +} + +#[derive(Component)] +pub(crate) struct Cursor { + timer: Timer, + visible: bool, +} + +impl Default for Cursor { + fn default() -> Self { + Self { + timer: Timer::from_seconds(0.5, TimerMode::Repeating), + visible: true, + } + } +} + +pub(crate) fn update_cursor(time: Res