From 3b0e31dd34d6fcc969ea9a418c65cf52454f0968 Mon Sep 17 00:00:00 2001 From: b-guild Date: Thu, 20 Feb 2025 22:18:26 -0800 Subject: [PATCH] Introducing tile map brush macros and autotile --- Cargo.toml | 3 +- editor/src/plugins/tilemap/autotile.rs | 657 +++++++++++++ editor/src/plugins/tilemap/brush_macro.rs | 857 +++++++++++++++++ editor/src/plugins/tilemap/commands.rs | 76 +- .../src/plugins/tilemap/interaction_mode.rs | 250 ++++- editor/src/plugins/tilemap/macro_inspector.rs | 435 +++++++++ editor/src/plugins/tilemap/macro_tab.rs | 561 +++++++++++ editor/src/plugins/tilemap/mod.rs | 167 +++- editor/src/plugins/tilemap/palette.rs | 97 +- editor/src/plugins/tilemap/panel.rs | 2 +- editor/src/plugins/tilemap/panel_preview.rs | 2 +- editor/src/plugins/tilemap/tile_inspector.rs | 56 +- .../src/plugins/tilemap/tile_prop_editor.rs | 50 +- editor/src/plugins/tilemap/tileset.rs | 111 ++- editor/src/plugins/tilemap/wfc.rs | 887 ++++++++++++++++++ fyrox-autotile/Cargo.toml | 19 + fyrox-autotile/LICENSE | 21 + fyrox-autotile/README.md | 13 + fyrox-autotile/src/auto.rs | 608 ++++++++++++ fyrox-autotile/src/lib.rs | 707 ++++++++++++++ fyrox-autotile/src/wave.rs | 748 +++++++++++++++ fyrox-impl/Cargo.toml | 1 + fyrox-impl/src/lib.rs | 3 + fyrox-impl/src/scene/tilemap/autotile.rs | 508 ++++++++++ fyrox-impl/src/scene/tilemap/brush.rs | 165 +++- fyrox-impl/src/scene/tilemap/data.rs | 7 +- fyrox-impl/src/scene/tilemap/effect.rs | 2 +- fyrox-impl/src/scene/tilemap/mod.rs | 48 +- fyrox-impl/src/scene/tilemap/property.rs | 111 ++- fyrox-impl/src/scene/tilemap/tile_source.rs | 113 ++- fyrox-impl/src/scene/tilemap/tileset.rs | 171 +++- fyrox-impl/src/scene/tilemap/transform.rs | 12 +- fyrox-impl/src/scene/tilemap/update.rs | 166 +++- 33 files changed, 7342 insertions(+), 292 deletions(-) create mode 100644 editor/src/plugins/tilemap/autotile.rs create mode 100644 editor/src/plugins/tilemap/brush_macro.rs create mode 100644 editor/src/plugins/tilemap/macro_inspector.rs create mode 100644 editor/src/plugins/tilemap/macro_tab.rs create mode 100644 editor/src/plugins/tilemap/wfc.rs create mode 100644 fyrox-autotile/Cargo.toml create mode 100644 fyrox-autotile/LICENSE create mode 100644 fyrox-autotile/README.md create mode 100644 fyrox-autotile/src/auto.rs create mode 100644 fyrox-autotile/src/lib.rs create mode 100644 fyrox-autotile/src/wave.rs create mode 100644 fyrox-impl/src/scene/tilemap/autotile.rs diff --git a/Cargo.toml b/Cargo.toml index ac2d22352..cdb9106b7 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -19,7 +19,8 @@ members = [ "project-manager", "fyrox-graphics", "fyrox-build-tools", - "fyrox-texture"] + "fyrox-texture", + "fyrox-autotile"] resolver = "2" [profile.dev] diff --git a/editor/src/plugins/tilemap/autotile.rs b/editor/src/plugins/tilemap/autotile.rs new file mode 100644 index 000000000..00ac01e5c --- /dev/null +++ b/editor/src/plugins/tilemap/autotile.rs @@ -0,0 +1,657 @@ +// Copyright (c) 2019-present Dmitry Stepanov and Fyrox Engine contributors. +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in all +// copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +// SOFTWARE. + +use fyrox::{ + asset::{ + untyped::{ResourceKind, UntypedResource}, + Resource, ResourceData, + }, + autotile::{ConstraintFillRules, NeededTerrain, TerrainSource}, + core::swap_hash_map_entry, + fxhash::FxHashMap, + gui::{ + check_box::{CheckBoxBuilder, CheckBoxMessage}, + formatted_text::WrapMode, + stack_panel::StackPanelBuilder, + }, + rand::thread_rng, + scene::tilemap::{ + brush::TileMapBrushResource, + tileset::{ + TileSetPropertyF32, TileSetPropertyId, TileSetPropertyNine, TileSetPropertyType, + TileSetPropertyValueElement, + }, + MacroTilesUpdate, TileSetAutoTileConstraint, TileSetAutoTileContext, TileSetAutoTiler, + TileSetConstraintMap, TileSetPatternSource, TileTerrainId, + }, +}; + +use crate::{ + command::{Command, CommandContext, CommandTrait}, + send_sync_message, +}; + +use super::*; + +const PATTERN_PROP_DESC: &str = concat!("Choose a nine-slice property from the tile set. ", + "This property will provide the pattern that the autotiler uses to know whether two tiles match along each edge. ", + "The central value of the nine is used to determine which tiles are permited in particular cell and how tiles are prioritized."); + +const FREQUENCY_PROP_DESC: &str = concat!("Choose a float property from the tile set. ", + "This property will provide the frequency that the autotiler uses to know know often to choose a tile when there is more than one ", + "tile with the same pattern."); + +#[derive(Default)] +pub struct AutoTileMacro { + pattern_list: MacroPropertyField, + frequency_list: MacroPropertyField, + context: TileSetAutoTileContext, + constraints: TileSetConstraintMap, + autotiler: TileSetAutoTiler, +} + +#[derive(Default, Debug, Clone, Visit, Reflect)] +struct CellData { + terrain_id: TileTerrainId, + fill: ConstraintFillRules, +} + +#[derive(Debug, Default, Clone, Visit, Reflect, TypeUuidProvider)] +#[type_uuid(id = "b320543d-3df0-43fd-b0d9-60a398f49853")] +pub(super) struct AutoTileInstance { + frequency_property: Option, + pattern_property: Option, + cells: FxHashMap, + #[reflect(hidden)] + #[visit(skip)] + widgets: InstanceCellWidgets, +} + +#[derive(Debug, Default, Clone)] +struct InstanceCellWidgets { + handle: Handle, + value_field: MacroPropertyValueField, + adjacent_toggle: Handle, + diagonal_toggle: Handle, +} + +impl ResourceData for AutoTileInstance { + fn type_uuid(&self) -> Uuid { + ::type_uuid() + } + + fn save(&mut self, _path: &std::path::Path) -> Result<(), Box> { + Err("Saving is not supported!".to_string().into()) + } + + fn can_be_saved(&self) -> bool { + false + } +} + +pub struct TileSetTerrainSource<'a, 'b> { + update: &'a MacroTilesUpdate, + instance: &'b AutoTileInstance, +} + +impl TerrainSource for TileSetTerrainSource<'_, '_> { + type Position = Vector2; + type Terrain = TileTerrainId; + + fn iter(&self) -> impl Iterator, TileTerrainId>> + '_ { + self.update.iter().filter_map(|(p, v)| { + let cell_data = self.instance.cells.get(v.as_ref()?.brush_cell.as_ref()?)?; + Some(NeededTerrain { + position: *p, + terrain: cell_data.terrain_id, + fill: cell_data.fill, + }) + }) + } + + fn contains_position(&self, position: &Self::Position) -> bool { + let Some(el) = self.update.get(position).cloned().flatten() else { + return false; + }; + let Some(brush_cell) = el.brush_cell else { + return false; + }; + self.instance.cells.contains_key(&brush_cell) + } +} + +impl BrushMacro for AutoTileMacro { + fn uuid(&self) -> &Uuid { + &uuid!("ab05f992-2591-4729-8164-7b5cc1141d72") + } + + fn name(&self) -> &'static str { + "Autotiler" + } + + fn on_instance_ui_message( + &mut self, + context: &BrushMacroInstance, + message: &UiMessage, + editor: &mut Editor, + ) { + let ui = editor.engine.user_interfaces.first_mut(); + let Some(tile_set) = context.tile_set() else { + return; + }; + if let Some(TileSetPropertyMessage(uuid)) = message.data() { + if message.destination() == self.pattern_list.handle() { + editor.message_sender.do_command(SetPatternPropCommand { + brush: context.brush.clone(), + instance: context.settings().unwrap(), + data: uuid.map(TileSetPropertyNine), + }); + } else if message.destination() == self.frequency_list.handle() { + editor.message_sender.do_command(SetFrequencyPropCommand { + brush: context.brush.clone(), + instance: context.settings().unwrap(), + data: uuid.map(TileSetPropertyF32), + }); + } + } else { + self.pattern_list + .on_ui_message(&tile_set.data_ref(), message, ui); + self.frequency_list + .on_ui_message(&tile_set.data_ref(), message, ui); + } + } + + fn on_cell_ui_message( + &mut self, + context: &MacroMessageContext, + message: &UiMessage, + editor: &mut Editor, + ) { + let ui = editor.engine.user_interfaces.first_mut(); + for r in context.instances_with_uuid(*self.uuid()) { + let instance = r.try_cast::().unwrap(); + let mut settings = instance.data_ref(); + let prop_id = settings + .pattern_property + .as_ref() + .map(|prop| prop.property_uuid()); + let tile_set = context.tile_set(); + let tile_set = tile_set.as_ref().map(|t| t.data_ref()); + let prop = + prop_id.and_then(|id| tile_set.as_ref().and_then(|set| set.find_property(*id))); + settings + .widgets + .value_field + .on_ui_message(prop, message, ui); + let cell = context.cell.unwrap(); + let cell_data = settings.cells.get(&cell).cloned().unwrap_or_default(); + if let Some(&TileSetPropertyValueMessage(TileSetPropertyValueElement::I8(v))) = + message.data() + { + if message.destination() == settings.widgets.value_field.handle() + && context.cell.is_some() + { + drop(settings); + editor.message_sender.do_command(SetCellCommand { + brush: context.brush.clone(), + instance, + cell, + data: Some(CellData { + terrain_id: v, + ..cell_data + }), + }); + } + } else if let Some(&CheckBoxMessage::Check(Some(checked))) = message.data() { + if message.destination() == settings.widgets.adjacent_toggle { + drop(settings); + editor.message_sender.do_command(SetCellCommand { + brush: context.brush.clone(), + instance, + cell, + data: Some(CellData { + fill: ConstraintFillRules { + include_adjacent: checked, + ..cell_data.fill + }, + ..cell_data + }), + }); + } else if message.destination() == settings.widgets.diagonal_toggle { + drop(settings); + editor.message_sender.do_command(SetCellCommand { + brush: context.brush.clone(), + instance, + cell, + data: Some(CellData { + fill: ConstraintFillRules { + include_diagonal: checked, + ..cell_data.fill + }, + ..cell_data + }), + }); + } + } + } + } + + fn create_instance(&self, _brush: &TileMapBrushResource) -> Option { + Some(UntypedResource::new_ok( + ResourceKind::Embedded, + AutoTileInstance::default(), + )) + } + + fn can_create_cell(&self) -> bool { + true + } + + fn fill_cell_set( + &self, + context: &BrushMacroInstance, + cell_set: &mut FxHashSet, + ) { + let Some(data) = context + .settings + .as_ref() + .and_then(|r| r.try_cast::()) + else { + return; + }; + let data = data.data_ref(); + cell_set.extend(data.cells.keys()); + } + + fn create_cell(&self, context: &BrushMacroCell) -> Option { + let instance = context.settings()?; + let cell = context.cell?; + Some(Command::new(SetCellCommand { + brush: context.brush.clone(), + cell, + instance, + data: Some(CellData::default()), + })) + } + + fn remove_cell(&self, context: &BrushMacroCell) -> Option { + let instance = context.settings()?; + let cell = context.cell?; + Some(Command::new(SetCellCommand { + brush: context.brush.clone(), + cell, + instance, + data: None, + })) + } + + fn build_instance_editor( + &mut self, + context: &BrushMacroInstance, + ctx: &mut BuildContext, + ) -> Option> { + let instance = context.settings::().unwrap(); + let instance = instance.data_ref(); + let pattern_id = instance + .pattern_property + .as_ref() + .map(|p| p.property_uuid()); + let frequency_id = instance + .frequency_property + .as_ref() + .map(|p| p.property_uuid()); + let tile_set = context.tile_set(); + let tile_set = tile_set.as_ref().map(|t| t.data_ref()); + let tile_set = tile_set.as_deref(); + self.pattern_list = MacroPropertyField::new( + WidgetBuilder::new().with_margin(Thickness::uniform(5.0)), + "Pattern Property".into(), + TileSetPropertyType::NineSlice, + pattern_id, + tile_set, + ctx, + ); + self.frequency_list = MacroPropertyField::new( + WidgetBuilder::new().with_margin(Thickness::uniform(5.0)), + "Frequency Property".into(), + TileSetPropertyType::F32, + frequency_id, + tile_set, + ctx, + ); + let pattern_prop_help_text = + TextBuilder::new(WidgetBuilder::new().with_margin(Thickness::uniform(5.0))) + .with_wrap(WrapMode::Word) + .with_text(PATTERN_PROP_DESC) + .build(ctx); + let freq_prop_help_text = + TextBuilder::new(WidgetBuilder::new().with_margin(Thickness::uniform(5.0))) + .with_wrap(WrapMode::Word) + .with_text(FREQUENCY_PROP_DESC) + .build(ctx); + let handle = StackPanelBuilder::new( + WidgetBuilder::new() + .with_margin(Thickness::uniform(5.0)) + .with_child(pattern_prop_help_text) + .with_child(self.pattern_list.handle()) + .with_child(freq_prop_help_text) + .with_child(self.frequency_list.handle()), + ) + .build(ctx); + Some(handle) + } + + fn build_cell_editor( + &mut self, + context: &BrushMacroCell, + ctx: &mut BuildContext, + ) -> Option> { + let instance = context.settings::().unwrap(); + let mut instance = instance.data_ref(); + let prop_id = instance + .pattern_property + .as_ref() + .map(|p| p.property_uuid()); + let value = context.cell.and_then(|c| instance.cells.get(&c)); + let tile_set = context.tile_set(); + let tile_set = tile_set.as_ref().map(|set| set.data_ref()); + let prop = if let (Some(tile_set), Some(uuid)) = (tile_set.as_ref(), prop_id) { + tile_set.find_property(*uuid) + } else { + None + }; + let terrain_id = value.map(|d| d.terrain_id).unwrap_or_default(); + let adjacent = value.map(|d| d.fill.include_adjacent).unwrap_or_default(); + let diagonal = value.map(|d| d.fill.include_diagonal).unwrap_or_default(); + let value_field = MacroPropertyValueField::new( + WidgetBuilder::new(), + "Terrain".into(), + TileSetPropertyValueElement::I8(terrain_id), + prop, + ctx, + ); + let adjacent_toggle = CheckBoxBuilder::new(WidgetBuilder::new()) + .checked(Some(adjacent)) + .build(ctx); + let diagonal_toggle = CheckBoxBuilder::new(WidgetBuilder::new()) + .checked(Some(diagonal)) + .build(ctx); + let adjacent_field = GridBuilder::new( + WidgetBuilder::new() + .with_child( + TextBuilder::new(WidgetBuilder::new().on_column(1)) + .with_text("Adjacent") + .build(ctx), + ) + .with_child(adjacent_toggle), + ) + .add_row(Row::auto()) + .add_column(Column::strict(20.0)) + .add_column(Column::stretch()) + .build(ctx); + let diagonal_field = GridBuilder::new( + WidgetBuilder::new() + .with_child( + TextBuilder::new(WidgetBuilder::new().on_column(1)) + .with_text("Diagonal") + .build(ctx), + ) + .with_child(diagonal_toggle), + ) + .add_row(Row::auto()) + .add_column(Column::strict(20.0)) + .add_column(Column::stretch()) + .build(ctx); + let handle = StackPanelBuilder::new( + WidgetBuilder::new() + .with_visibility(value.is_some()) + .with_child(value_field.handle()) + .with_child(adjacent_field) + .with_child(diagonal_field), + ) + .build(ctx); + instance.widgets.handle = handle; + instance.widgets.value_field = value_field; + instance.widgets.adjacent_toggle = adjacent_toggle; + instance.widgets.diagonal_toggle = diagonal_toggle; + Some(handle) + } + + fn sync_instance_editor(&mut self, context: &BrushMacroInstance, ui: &mut UserInterface) { + let Some(instance) = context.settings::() else { + return; + }; + let instance = instance.data_ref(); + let pattern_id = instance + .pattern_property + .as_ref() + .map(|p| p.property_uuid()); + let frequency_id = instance + .frequency_property + .as_ref() + .map(|p| p.property_uuid()); + let tile_set = context.tile_set(); + let tile_set = tile_set.as_ref().map(|t| t.data_ref()); + let tile_set = tile_set.as_deref(); + self.pattern_list.sync(pattern_id, tile_set, ui); + self.frequency_list.sync(frequency_id, tile_set, ui); + } + + fn sync_cell_editors(&mut self, context: &MacroMessageContext, ui: &mut UserInterface) { + for r in context.instances_with_uuid(*self.uuid()) { + let settings = r.try_cast::().unwrap(); + let settings = settings.data_ref(); + let prop_id = settings + .pattern_property + .as_ref() + .map(|prop| prop.property_uuid()); + let tile_set = context.tile_set(); + let tile_set = tile_set.as_ref().map(|t| t.data_ref()); + let prop = + prop_id.and_then(|id| tile_set.as_ref().and_then(|set| set.find_property(*id))); + let cell_data = context + .cell + .as_ref() + .and_then(|cell| settings.cells.get(cell)); + let value = cell_data.map(|cd| cd.terrain_id).unwrap_or_default(); + let value = TileSetPropertyValueElement::I8(value); + let adjacent = cell_data.map(|d| d.fill.include_adjacent); + let diagonal = cell_data.map(|d| d.fill.include_diagonal); + settings.widgets.value_field.sync(value, prop, ui); + send_sync_message( + ui, + CheckBoxMessage::checked( + settings.widgets.adjacent_toggle, + MessageDirection::ToWidget, + adjacent, + ), + ); + send_sync_message( + ui, + CheckBoxMessage::checked( + settings.widgets.diagonal_toggle, + MessageDirection::ToWidget, + diagonal, + ), + ); + ui.send_message(WidgetMessage::visibility( + settings.widgets.handle, + MessageDirection::ToWidget, + cell_data.is_some(), + )); + } + } + + fn begin_update(&mut self, context: &BrushMacroInstance, tile_map: &TileMapContext) { + let Some(tile_set) = tile_map.tile_set() else { + self.context.clear(); + return; + }; + if context.tile_set().as_deref() != Some(tile_set) { + self.context.clear(); + return; + } + let instance = context.settings::().unwrap(); + let instance = instance.data_ref(); + let Some(pattern_property) = instance.pattern_property else { + self.context.clear(); + return; + }; + let frequency_property = instance.frequency_property; + Log::verify(self.context.fill_pattern_map( + &tile_set.data_ref(), + pattern_property, + frequency_property, + )); + } + + fn amend_update( + &mut self, + context: &BrushMacroInstance, + update: &mut MacroTilesUpdate, + tile_map: &TileMap, + ) { + let Some(instance) = &context.settings() else { + return; + }; + let instance = instance.data_ref(); + let instance = &instance; + let terrains = TileSetTerrainSource { update, instance }; + let Some(tile_set) = tile_map.tile_set() else { + return; + }; + let tile_set = tile_set.data_ref(); + let tile_set = &tile_set; + let Some(property_id) = instance.pattern_property else { + return; + }; + let patterns = TileSetPatternSource { + tile_map, + tile_set, + update, + property_id, + }; + self.constraints.fill_from(&terrains, &patterns); + self.autotiler.clear(); + self.autotiler.autotile(&TileSetAutoTileConstraint { + position_constraints: &self.constraints, + pattern_constraints: &self.context.patterns, + }); + self.autotiler + .apply_autotile_to_update(&mut thread_rng(), &self.context.values, update); + } + + fn create_command( + &mut self, + _context: &BrushMacroInstance, + _update: &mut MacroTilesUpdate, + _tile_map: &TileMapContext, + ) -> Option { + None + } +} + +#[derive(Debug)] +struct SetCellCommand { + pub brush: TileMapBrushResource, + pub instance: Resource, + pub cell: TileDefinitionHandle, + pub data: Option, +} + +impl SetCellCommand { + fn swap(&mut self) { + let mut instance = self.instance.data_ref(); + swap_hash_map_entry(instance.cells.entry(self.cell), &mut self.data); + self.brush.data_ref().change_flag.set(); + } +} + +impl CommandTrait for SetCellCommand { + fn name(&mut self, _context: &dyn CommandContext) -> String { + "Update Autotile Cell".into() + } + + fn execute(&mut self, _context: &mut dyn CommandContext) { + self.swap(); + } + + fn revert(&mut self, _context: &mut dyn CommandContext) { + self.swap(); + } +} + +#[derive(Debug)] +struct SetPatternPropCommand { + pub brush: TileMapBrushResource, + pub instance: Resource, + pub data: Option, +} + +impl SetPatternPropCommand { + fn swap(&mut self) { + let mut instance = self.instance.data_ref(); + std::mem::swap(&mut instance.pattern_property, &mut self.data); + self.brush.data_ref().change_flag.set(); + } +} + +impl CommandTrait for SetPatternPropCommand { + fn name(&mut self, _context: &dyn CommandContext) -> String { + "Update Autotile Property".into() + } + + fn execute(&mut self, _context: &mut dyn CommandContext) { + self.swap(); + } + + fn revert(&mut self, _context: &mut dyn CommandContext) { + self.swap(); + } +} + +#[derive(Debug)] +struct SetFrequencyPropCommand { + pub brush: TileMapBrushResource, + pub instance: Resource, + pub data: Option, +} + +impl SetFrequencyPropCommand { + fn swap(&mut self) { + let mut instance = self.instance.data_ref(); + std::mem::swap(&mut instance.frequency_property, &mut self.data); + self.brush.data_ref().change_flag.set(); + } +} + +impl CommandTrait for SetFrequencyPropCommand { + fn name(&mut self, _context: &dyn CommandContext) -> String { + "Update Autotile Property".into() + } + + fn execute(&mut self, _context: &mut dyn CommandContext) { + self.swap(); + } + + fn revert(&mut self, _context: &mut dyn CommandContext) { + self.swap(); + } +} diff --git a/editor/src/plugins/tilemap/brush_macro.rs b/editor/src/plugins/tilemap/brush_macro.rs new file mode 100644 index 000000000..5bc36fcfb --- /dev/null +++ b/editor/src/plugins/tilemap/brush_macro.rs @@ -0,0 +1,857 @@ +// Copyright (c) 2019-present Dmitry Stepanov and Fyrox Engine contributors. +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in all +// copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +// SOFTWARE. + +use fyrox::{ + asset::{untyped::UntypedResource, Resource, ResourceData, ResourceDataRef}, + core::{ + futures::executor::block_on, + log::Log, + parking_lot::{lock_api::MutexGuard, RawMutex}, + }, + gui::{ + dropdown_list::{DropdownListBuilder, DropdownListMessage}, + grid::{Column, GridBuilder, Row}, + numeric::{NumericUpDownBuilder, NumericUpDownMessage}, + text::{TextBuilder, TextMessage}, + text_box::TextBoxBuilder, + }, + scene::tilemap::{ + brush::{BrushMacroData, TileMapBrush, TileMapBrushResource}, + tileset::{ + NamableValue, TileSetPropertyLayer, TileSetPropertyType, TileSetPropertyValueElement, + }, + MacroTilesUpdate, + }, +}; + +use crate::{ + command::{Command, CommandContext, CommandTrait}, + send_sync_message, +}; + +use super::*; + +const PROPERTY_LABEL_WIDTH: f32 = 150.0; +const UNKNOWN_PROPERTY: &str = "UNKNOWN PROPERTY"; + +#[derive(Default, Clone)] +pub struct BrushMacroListRef(Arc>); + +impl BrushMacroListRef { + pub fn new(list: BrushMacroList) -> Self { + Self(Arc::new(Mutex::new(list))) + } + pub fn lock(&self) -> MutexGuard { + self.0 + .try_lock() + .expect("BrushMacroList lock should not fail") + } +} + +#[derive(Default, Clone)] +pub struct MacroCellSetListRef(Arc>); + +impl MacroCellSetListRef { + pub fn new(list: MacroCellSetList) -> Self { + Self(Arc::new(Mutex::new(list))) + } + pub fn lock(&self) -> MutexGuard { + self.0.try_lock().expect("MacroCellSetList already in use") + } +} + +pub struct TileMapContext<'a> { + pub node: Handle, + pub scene: Handle, + pub engine: &'a mut Engine, +} + +impl TileMapContext<'_> { + pub fn tile_map(&self) -> &TileMap { + self.engine.scenes[self.scene].graph[self.node] + .cast() + .unwrap() + } + pub fn tile_set(&self) -> Option<&TileSetResource> { + self.tile_map().tile_set() + } +} + +#[derive(Debug, Clone)] +pub struct BrushMacroInstance { + /// The brush that has an instance of the macro. + pub brush: TileMapBrushResource, + /// The configuration for an instance of the macro. + pub settings: Option, +} + +impl BrushMacroInstance { + pub fn settings(&self) -> Option> + where + T: ResourceData + Default + TypeUuidProvider, + { + self.settings.as_ref()?.try_cast() + } + pub fn brush(&self) -> ResourceDataRef { + self.brush.data_ref() + } + pub fn tile_set(&self) -> Option { + TileSetFromBrush::try_new(&self.brush) + } +} + +#[derive(Debug, Clone)] +pub struct MacroMessageContext { + /// The brush that has an instance of the macro. + pub brush: TileMapBrushResource, + /// The currently selected brush cell, or None if no cell is selected. + pub cell: Option, +} + +impl From for MacroMessageContext { + fn from(value: BrushMacroCell) -> Self { + Self { + brush: value.brush, + cell: value.cell, + } + } +} + +impl MacroMessageContext { + pub fn brush(&self) -> ResourceDataRef { + self.brush.data_ref() + } + pub fn tile_set(&self) -> Option { + TileSetFromBrush::try_new(&self.brush) + } + pub fn instances_with_uuid(&self, uuid: Uuid) -> Vec { + self.brush + .data_ref() + .macros + .instances_with_uuid(uuid) + .cloned() + .collect() + } +} + +#[derive(Debug, Clone)] +pub struct BrushMacroCell { + /// The brush that has an instance of the macro. + pub brush: TileMapBrushResource, + /// The configuration for an instance of the macro. + pub settings: Option, + /// The currently selected brush cell, or None if no cell is selected. + pub cell: Option, +} + +impl From for BrushMacroInstance { + fn from(value: BrushMacroCell) -> Self { + Self { + brush: value.brush, + settings: value.settings, + } + } +} + +impl BrushMacroCell { + pub fn settings(&self) -> Option> + where + T: ResourceData + Default + TypeUuidProvider, + { + self.settings.as_ref()?.try_cast() + } + pub fn brush(&self) -> ResourceDataRef { + self.brush.data_ref() + } + pub fn tile_set(&self) -> Option { + TileSetFromBrush::try_new(&self.brush) + } +} +pub struct TileSetFromBrush<'a>(ResourceDataRef<'a, TileMapBrush>); + +impl Deref for TileSetFromBrush<'_> { + type Target = Resource; + + fn deref(&self) -> &Self::Target { + self.0.tile_set.as_ref().unwrap() + } +} + +impl<'a> TileSetFromBrush<'a> { + pub fn try_new(brush: &'a TileMapBrushResource) -> Option { + Log::verify(block_on(brush.clone())); + if !brush.is_ok() { + return None; + } + let brush_guard = brush.data_ref(); + let tile_set = brush_guard.tile_set.clone(); + drop(brush_guard); + if let Some(tile_set) = tile_set { + Log::verify(block_on(tile_set.clone())); + if !tile_set.is_ok() { + return None; + } + } + Some(Self(brush.data_ref())) + } +} + +pub trait BrushMacro: 'static + Send + Sync { + fn uuid(&self) -> &Uuid; + fn name(&self) -> &'static str; + fn on_instance_ui_message( + &mut self, + context: &BrushMacroInstance, + message: &UiMessage, + editor: &mut Editor, + ); + fn on_cell_ui_message( + &mut self, + context: &MacroMessageContext, + message: &UiMessage, + editor: &mut Editor, + ); + /// Create a resource to store the settings of this macro. + /// A return value of None indicates that his macro needs no settings resource. + /// An instance will still be created without the resource. + fn create_instance(&self, brush: &TileMapBrushResource) -> Option; + /// True if instances of this macro can contain particular brush cells. + fn can_create_cell(&self) -> bool; + /// Create a command to modify the given instance's data to include the given cell. + /// None is returned if no command is necessary, such as if the cell is already included + /// or no cell is selected. Adding the currently selected cell will naturally require + /// the widgets that edit the data to change, but that will wait until + /// [`BrushMacro::sync_cell_editor`] is called. + fn create_cell(&self, context: &BrushMacroCell) -> Option; + /// Create a command to modify the given instances's data to remove the given cell. + /// None is returned if no command is necessary, such as if the cell is already excluded + /// or no cell is selected. + fn remove_cell(&self, context: &BrushMacroCell) -> Option; + fn fill_cell_set( + &self, + context: &BrushMacroInstance, + cell_set: &mut FxHashSet, + ); + /// Build the widgets that will edit the configuration data for an instance of the + /// macro. The macro is modified to remember the handles of the widgets so that it + /// can respond correctly to UI messages. None is returned if this macro needs no + /// widgets because it cannot be edited. + fn build_instance_editor( + &mut self, + context: &BrushMacroInstance, + ctx: &mut BuildContext, + ) -> Option>; + /// Build the widgets that will edit the configuration data for an instance of the + /// macro and for a particular cell of the brush. + /// The macro is modified to remember the handles of the widgets so that it + /// can respond correctly to UI messages. Each instance may have it own separate widgets, + /// so the handles must be remembered for every instance. + /// None is returned if this macro needs no widgets because cell data cannot be edited. + /// If no cell is selected or if the cell is not part of the macro, then the widgets + /// should stil be created but they should be invisible. They can be made visible + /// when [`BrushMacro::sync_cell_editor`] is called. + fn build_cell_editor( + &mut self, + context: &BrushMacroCell, + ctx: &mut BuildContext, + ) -> Option>; + /// Send the necessary messages to update the cell editor widgets to edit the data for + /// the given instance. + fn sync_instance_editor(&mut self, context: &BrushMacroInstance, ui: &mut UserInterface); + /// Send the necessary messages to update the cell editor widgets to edit the data for + /// the given cell, or make those widgets invisible if there is no selected cell or the + /// cell has no data. + fn sync_cell_editors(&mut self, context: &MacroMessageContext, ui: &mut UserInterface); + fn begin_update(&mut self, context: &BrushMacroInstance, tile_map: &TileMapContext); + fn amend_update( + &mut self, + context: &BrushMacroInstance, + update: &mut MacroTilesUpdate, + tile_map: &TileMap, + ); + fn create_command( + &mut self, + context: &BrushMacroInstance, + update: &mut MacroTilesUpdate, + tile_map: &TileMapContext, + ) -> Option; +} + +pub struct BrushMacroList { + content: Vec>, +} + +impl Default for BrushMacroList { + fn default() -> Self { + let mut result = Self { + content: Default::default(), + }; + result.add(AutoTileMacro::default()); + result.add(WfcMacro::default()); + result + } +} + +impl BrushMacroList { + #[inline] + pub fn add(&mut self, brush_macro: T) { + self.content + .push(Box::new(brush_macro) as Box); + } + #[inline] + pub fn is_empty(&self) -> bool { + self.content.is_empty() + } + #[inline] + pub fn len(&self) -> usize { + self.content.len() + } + #[inline] + pub fn iter(&self) -> impl Iterator { + self.content.iter().map(|b| b.as_ref()) + } + #[inline] + pub fn iter_mut(&mut self) -> impl Iterator { + self.content.iter_mut().map(|b| b.as_mut()) + } + #[inline] + pub fn get_by_index(&self, index: usize) -> Option<&dyn BrushMacro> { + self.content.get(index).map(|b| b.as_ref()) + } + #[inline] + pub fn get_by_index_mut(&mut self, index: usize) -> Option<&mut dyn BrushMacro> { + self.content.get_mut(index).map(|b| b.as_mut()) + } + pub fn get_by_uuid(&self, uuid: &Uuid) -> Option<&dyn BrushMacro> { + for brush_macro in self.content.iter() { + if brush_macro.uuid() == uuid { + return Some(brush_macro.as_ref()); + } + } + None + } + pub fn get_by_uuid_mut(&mut self, uuid: &Uuid) -> Option<&mut dyn BrushMacro> { + for brush_macro in self.content.iter_mut() { + if brush_macro.uuid() == uuid { + return Some(brush_macro.as_mut()); + } + } + None + } + #[inline] + pub fn clear(&mut self) { + self.content.clear(); + } +} + +#[derive(Debug)] +pub struct MoveMacroCommand { + pub brush: TileMapBrushResource, + pub start: usize, + pub end: usize, +} + +impl CommandTrait for MoveMacroCommand { + fn name(&mut self, _context: &dyn CommandContext) -> String { + "Move Brush Macro".into() + } + + fn execute(&mut self, _context: &mut dyn CommandContext) { + let mut brush = self.brush.data_ref(); + brush.macros.swap(self.start, self.end); + brush.change_flag.set(); + } + + fn revert(&mut self, _context: &mut dyn CommandContext) { + let mut brush = self.brush.data_ref(); + brush.macros.swap(self.start, self.end); + brush.change_flag.set(); + } +} + +#[derive(Debug)] +pub struct RemoveMacroCommand { + pub brush: TileMapBrushResource, + pub index: usize, + pub data: Option, +} + +impl CommandTrait for RemoveMacroCommand { + fn name(&mut self, _context: &dyn CommandContext) -> String { + "Remove Brush Macro".into() + } + + fn execute(&mut self, _context: &mut dyn CommandContext) { + let mut brush = self.brush.data_ref(); + self.data = Some(brush.macros.remove(self.index)); + brush.change_flag.set(); + } + + fn revert(&mut self, _context: &mut dyn CommandContext) { + let mut brush = self.brush.data_ref(); + if let Some(data) = self.data.take() { + brush.macros.insert(self.index, data); + brush.change_flag.set(); + } + } +} + +#[derive(Debug)] +pub struct AddMacroCommand { + pub brush: TileMapBrushResource, + pub index: usize, + pub data: Option, +} + +impl CommandTrait for AddMacroCommand { + fn name(&mut self, _context: &dyn CommandContext) -> String { + "Add Brush Macro".into() + } + + fn execute(&mut self, _context: &mut dyn CommandContext) { + let mut brush = self.brush.data_ref(); + if let Some(data) = self.data.take() { + brush.macros.insert(self.index, data); + brush.change_flag.set(); + } + } + + fn revert(&mut self, _context: &mut dyn CommandContext) { + let mut brush = self.brush.data_ref(); + self.data = Some(brush.macros.remove(self.index)); + brush.change_flag.set(); + } +} + +#[derive(Debug)] +pub struct SetMacroNameCommand { + pub brush: TileMapBrushResource, + pub index: usize, + pub name: String, +} + +impl SetMacroNameCommand { + fn swap(&mut self) { + let mut brush = self.brush.data_ref(); + let brush_macro = brush.macros.get_mut(self.index).unwrap(); + std::mem::swap(&mut brush_macro.name, &mut self.name); + brush.change_flag.set(); + } +} + +impl CommandTrait for SetMacroNameCommand { + fn name(&mut self, _context: &dyn CommandContext) -> String { + "Rename Brush Macro".into() + } + + fn execute(&mut self, _context: &mut dyn CommandContext) { + self.swap(); + } + + fn revert(&mut self, _context: &mut dyn CommandContext) { + self.swap(); + } +} + +#[derive(Clone, Debug, PartialEq)] +pub struct TileSetPropertyValueMessage(pub TileSetPropertyValueElement); + +impl TileSetPropertyValueMessage { + pub fn value( + destination: Handle, + direction: MessageDirection, + value: TileSetPropertyValueElement, + ) -> UiMessage { + UiMessage::with_data(TileSetPropertyValueMessage(value)) + .with_destination(destination) + .with_direction(direction) + } +} + +#[derive(Clone, Debug, PartialEq)] +pub struct TileSetPropertyMessage(pub Option); + +impl TileSetPropertyMessage { + pub fn property_id( + destination: Handle, + direction: MessageDirection, + value: Option, + ) -> UiMessage { + UiMessage::with_data(TileSetPropertyMessage(value)) + .with_destination(destination) + .with_direction(direction) + } +} + +#[derive(Debug, Default, Clone)] +pub struct MacroPropertyValueField { + handle: Handle, + textbox: Handle, + list: Handle, +} + +fn make_index_and_value_list( + prop: Option<&TileSetPropertyLayer>, + value: NamableValue, + ctx: &mut BuildContext, +) -> (usize, Vec>) { + let index; + let items; + if let Some(prop) = prop { + index = prop + .find_value_index(value) + .map(|i| i + 1) + .unwrap_or_default(); + items = make_named_value_list_items(prop, ctx); + } else { + index = 0; + items = vec![make_named_value_list_option( + ctx, + ELEMENT_MATCH_HIGHLIGHT_COLOR.to_opaque(), + "Custom", + )]; + } + (index, items) +} + +fn find_list_index(prop: Option<&TileSetPropertyLayer>, value: NamableValue) -> usize { + if let Some(prop) = prop { + prop.find_value_index(value) + .map(|i| i + 1) + .unwrap_or_default() + } else { + 0 + } +} + +impl MacroPropertyValueField { + pub fn new( + widget_builder: WidgetBuilder, + label: String, + value: TileSetPropertyValueElement, + prop: Option<&TileSetPropertyLayer>, + ctx: &mut BuildContext, + ) -> Self { + use TileSetPropertyValueElement as Element; + let label = TextBuilder::new(WidgetBuilder::new()) + .with_text(label) + .build(ctx); + let wb = WidgetBuilder::new().on_column(1); + let textbox = match &value { + Element::I32(v) => NumericUpDownBuilder::::new(wb) + .with_value(*v) + .build(ctx), + Element::F32(v) => NumericUpDownBuilder::::new(wb) + .with_value(*v) + .build(ctx), + Element::I8(v) => NumericUpDownBuilder::::new(wb) + .with_value(*v) + .build(ctx), + Element::String(v) => TextBoxBuilder::new(wb).with_text(v).build(ctx), + }; + let list = if let Ok(value) = value.try_into() { + let (index, items) = make_index_and_value_list(prop, value, ctx); + DropdownListBuilder::new(WidgetBuilder::new().on_column(1).on_row(1)) + .with_items(items) + .with_selected(index) + .build(ctx) + } else { + Handle::NONE + }; + let handle = GridBuilder::new( + widget_builder + .with_child(label) + .with_child(textbox) + .with_child(list), + ) + .add_column(Column::strict(PROPERTY_LABEL_WIDTH)) + .add_column(Column::stretch()) + .add_row(Row::auto()) + .add_row(Row::auto()) + .build(ctx); + Self { + handle, + textbox, + list, + } + } + pub fn handle(&self) -> Handle { + self.handle + } + pub fn sync( + &self, + value: TileSetPropertyValueElement, + prop: Option<&TileSetPropertyLayer>, + ui: &mut UserInterface, + ) { + use TileSetPropertyValueElement as Element; + let msg = match &value { + Element::I32(v) => { + NumericUpDownMessage::value(self.textbox, MessageDirection::ToWidget, *v) + } + Element::F32(v) => { + NumericUpDownMessage::value(self.textbox, MessageDirection::ToWidget, *v) + } + Element::String(v) => { + TextMessage::text(self.textbox, MessageDirection::ToWidget, v.to_string()) + } + Element::I8(v) => { + NumericUpDownMessage::value(self.textbox, MessageDirection::ToWidget, *v) + } + }; + send_sync_message(ui, msg); + if let Ok(value) = value.try_into() { + let (index, items) = make_index_and_value_list(prop, value, &mut ui.build_ctx()); + ui.send_message(DropdownListMessage::items( + self.list, + MessageDirection::ToWidget, + items, + )); + send_sync_message( + ui, + DropdownListMessage::selection(self.list, MessageDirection::ToWidget, Some(index)), + ); + } + } + pub fn on_ui_message( + &mut self, + prop: Option<&TileSetPropertyLayer>, + message: &UiMessage, + ui: &mut UserInterface, + ) { + if let Some(TextMessage::Text(text)) = message.data() { + if message.destination() == self.textbox + && message.direction() == MessageDirection::FromWidget + { + ui.send_message(TileSetPropertyValueMessage::value( + self.handle, + MessageDirection::FromWidget, + TileSetPropertyValueElement::String(text.into()), + )); + } + } else if let Some(NumericUpDownMessage::::Value(v)) = message.data() { + if message.destination() == self.textbox + && message.direction() == MessageDirection::FromWidget + { + ui.send_message(TileSetPropertyValueMessage::value( + self.handle, + MessageDirection::FromWidget, + TileSetPropertyValueElement::I8(*v), + )); + send_sync_message( + ui, + DropdownListMessage::selection( + self.list, + MessageDirection::ToWidget, + Some(find_list_index(prop, NamableValue::I8(*v))), + ), + ); + } + } else if let Some(NumericUpDownMessage::::Value(v)) = message.data() { + if message.destination() == self.textbox + && message.direction() == MessageDirection::FromWidget + { + ui.send_message(TileSetPropertyValueMessage::value( + self.handle, + MessageDirection::FromWidget, + TileSetPropertyValueElement::I32(*v), + )); + send_sync_message( + ui, + DropdownListMessage::selection( + self.list, + MessageDirection::ToWidget, + Some(find_list_index(prop, NamableValue::I32(*v))), + ), + ); + } + } else if let Some(NumericUpDownMessage::::Value(v)) = message.data() { + if message.destination() == self.textbox + && message.direction() == MessageDirection::FromWidget + { + ui.send_message(TileSetPropertyValueMessage::value( + self.handle, + MessageDirection::FromWidget, + TileSetPropertyValueElement::F32(*v), + )); + send_sync_message( + ui, + DropdownListMessage::selection( + self.list, + MessageDirection::ToWidget, + Some(find_list_index(prop, NamableValue::F32(*v))), + ), + ); + } + } else if let Some(DropdownListMessage::SelectionChanged(Some(index))) = message.data() { + if message.destination() == self.list + && message.direction() == MessageDirection::FromWidget + && *index > 0 + { + if let Some(v) = prop.and_then(|p| p.named_values.get(index - 1)) { + ui.send_message(TileSetPropertyValueMessage::value( + self.handle, + MessageDirection::FromWidget, + v.value.into(), + )); + let msg = match v.value { + NamableValue::I32(v) => { + NumericUpDownMessage::value(self.textbox, MessageDirection::ToWidget, v) + } + NamableValue::F32(v) => { + NumericUpDownMessage::value(self.textbox, MessageDirection::ToWidget, v) + } + NamableValue::I8(v) => { + NumericUpDownMessage::value(self.textbox, MessageDirection::ToWidget, v) + } + }; + send_sync_message(ui, msg); + } + } + } + } +} + +fn make_item(text: &str, ctx: &mut BuildContext) -> Handle { + DecoratorBuilder::new(BorderBuilder::new( + WidgetBuilder::new().with_child( + TextBuilder::new(WidgetBuilder::new()) + .with_text(text) + .build(ctx), + ), + )) + .build(ctx) +} + +#[derive(Debug, Default)] +pub struct MacroPropertyField { + prop_type: TileSetPropertyType, + handle: Handle, + list: Handle, +} + +fn make_index_and_items( + prop_type: TileSetPropertyType, + value: Option<&Uuid>, + tile_set: Option<&TileSet>, + ctx: &mut BuildContext, +) -> (usize, Vec>) { + let mut items = vec![make_item("None", ctx)]; + let mut index = value.is_none().then_some(0); + if let Some(tile_set) = tile_set { + for (i, prop) in tile_set + .properties + .iter() + .filter(|p| p.prop_type == prop_type) + .enumerate() + { + if Some(&prop.uuid) == value { + index = Some(i + 1); + } + items.push(make_item(&prop.name, ctx)); + } + } + let index = if let Some(index) = index { + index + } else { + let index = items.len(); + items.push(make_item(UNKNOWN_PROPERTY, ctx)); + index + }; + (index, items) +} + +impl MacroPropertyField { + pub fn new( + widget_builder: WidgetBuilder, + label: String, + prop_type: TileSetPropertyType, + value: Option<&Uuid>, + tile_set: Option<&TileSet>, + ctx: &mut BuildContext, + ) -> Self { + let label = TextBuilder::new(WidgetBuilder::new()) + .with_text(label) + .build(ctx); + let (index, items) = make_index_and_items(prop_type, value, tile_set, ctx); + let list = DropdownListBuilder::new(WidgetBuilder::new().on_column(1)) + .with_items(items) + .with_selected(index) + .build(ctx); + let handle = GridBuilder::new(widget_builder.with_child(label).with_child(list)) + .add_column(Column::strict(PROPERTY_LABEL_WIDTH)) + .add_column(Column::stretch()) + .add_row(Row::auto()) + .build(ctx); + Self { + prop_type, + handle, + list, + } + } + pub fn handle(&self) -> Handle { + self.handle + } + pub fn sync(&self, value: Option<&Uuid>, tile_set: Option<&TileSet>, ui: &mut UserInterface) { + let (index, items) = + make_index_and_items(self.prop_type, value, tile_set, &mut ui.build_ctx()); + ui.send_message(DropdownListMessage::items( + self.list, + MessageDirection::ToWidget, + items, + )); + send_sync_message( + ui, + DropdownListMessage::selection(self.list, MessageDirection::ToWidget, Some(index)), + ); + } + pub fn on_ui_message( + &mut self, + tile_set: &TileSet, + message: &UiMessage, + ui: &mut UserInterface, + ) { + if let Some(DropdownListMessage::SelectionChanged(index)) = message.data() { + if message.destination() == self.list + && message.direction() == MessageDirection::FromWidget + { + let id = if let Some(index) = *index { + if index > 0 { + tile_set + .properties + .iter() + .filter(|p| p.prop_type == self.prop_type) + .nth(index - 1) + .map(|p| p.uuid) + } else { + None + } + } else { + None + }; + ui.send_message(TileSetPropertyMessage::property_id( + self.handle, + MessageDirection::FromWidget, + id, + )); + } + } + } +} diff --git a/editor/src/plugins/tilemap/commands.rs b/editor/src/plugins/tilemap/commands.rs index 34165eda9..e03ffd445 100644 --- a/editor/src/plugins/tilemap/commands.rs +++ b/editor/src/plugins/tilemap/commands.rs @@ -61,7 +61,7 @@ impl SetColliderLayerNameCommand { return; }; std::mem::swap(&mut collider.name, &mut self.name); - tile_set.change_count.set(); + tile_set.change_flag.set(); } } @@ -93,7 +93,7 @@ impl SetPropertyLayerNameCommand { return; }; std::mem::swap(&mut property.name, &mut self.name); - tile_set.change_count.set(); + tile_set.change_flag.set(); } } @@ -125,7 +125,7 @@ impl SetColliderLayerColorCommand { return; }; std::mem::swap(&mut collider.color, &mut self.color); - tile_set.change_count.set(); + tile_set.change_flag.set(); } } @@ -161,7 +161,7 @@ impl SetPropertyValueColorCommand { return; }; std::mem::swap(&mut named_value.color, &mut self.color); - tile_set.change_count.set(); + tile_set.change_flag.set(); } } @@ -197,7 +197,7 @@ impl SetPropertyValueNameCommand { return; }; std::mem::swap(&mut named_value.name, &mut self.name); - tile_set.change_count.set(); + tile_set.change_flag.set(); } } @@ -233,7 +233,7 @@ impl SetPropertyValueCommand { return; }; std::mem::swap(&mut named_value.value, &mut self.value); - tile_set.change_count.set(); + tile_set.change_flag.set(); } } @@ -282,7 +282,7 @@ impl CommandTrait for AddPropertyValueCommand { ..NamedValue::default() }, ); - tile_set.change_count.set(); + tile_set.change_flag.set(); } fn revert(&mut self, _context: &mut dyn CommandContext) { @@ -291,7 +291,7 @@ impl CommandTrait for AddPropertyValueCommand { return; }; property.named_values.remove(self.index); - tile_set.change_count.set(); + tile_set.change_flag.set(); } } @@ -314,7 +314,7 @@ impl CommandTrait for RemovePropertyValueCommand { return; }; self.value = Some(property.named_values.remove(self.index)); - tile_set.change_count.set(); + tile_set.change_flag.set(); } fn revert(&mut self, _context: &mut dyn CommandContext) { @@ -325,7 +325,7 @@ impl CommandTrait for RemovePropertyValueCommand { property .named_values .insert(self.index, self.value.take().unwrap()); - tile_set.change_count.set(); + tile_set.change_flag.set(); } } @@ -350,13 +350,13 @@ impl CommandTrait for AddColliderLayerCommand { ..TileSetColliderLayer::default() }, ); - tile_set.change_count.set(); + tile_set.change_flag.set(); } fn revert(&mut self, _context: &mut dyn CommandContext) { let mut tile_set = self.tile_set.data_ref(); tile_set.colliders.remove(self.index); - tile_set.change_count.set(); + tile_set.change_flag.set(); } } @@ -383,13 +383,13 @@ impl CommandTrait for AddPropertyLayerCommand { ..TileSetPropertyLayer::default() }, ); - tile_set.change_count.set(); + tile_set.change_flag.set(); } fn revert(&mut self, _context: &mut dyn CommandContext) { let mut tile_set = self.tile_set.data_ref(); tile_set.properties.remove(self.index); - tile_set.change_count.set(); + tile_set.change_flag.set(); } } @@ -408,13 +408,13 @@ impl CommandTrait for MovePropertyLayerCommand { fn execute(&mut self, _context: &mut dyn CommandContext) { let mut tile_set = self.tile_set.data_ref(); tile_set.properties.swap(self.start, self.end); - tile_set.change_count.set(); + tile_set.change_flag.set(); } fn revert(&mut self, _context: &mut dyn CommandContext) { let mut tile_set = self.tile_set.data_ref(); tile_set.properties.swap(self.start, self.end); - tile_set.change_count.set(); + tile_set.change_flag.set(); } } @@ -433,7 +433,7 @@ impl MovePropertyValueCommand { return; }; property.named_values.swap(self.start, self.end); - tile_set.change_count.set(); + tile_set.change_flag.set(); } } @@ -466,13 +466,13 @@ impl CommandTrait for MoveColliderLayerCommand { fn execute(&mut self, _context: &mut dyn CommandContext) { let mut tile_set = self.tile_set.data_ref(); tile_set.colliders.swap(self.start, self.end); - tile_set.change_count.set(); + tile_set.change_flag.set(); } fn revert(&mut self, _context: &mut dyn CommandContext) { let mut tile_set = self.tile_set.data_ref(); tile_set.colliders.swap(self.start, self.end); - tile_set.change_count.set(); + tile_set.change_flag.set(); } } @@ -495,7 +495,7 @@ impl CommandTrait for RemoveColliderLayerCommand { let uuid = layer.uuid; self.layer = Some(layer); tile_set.swap_all_values_for_collider(uuid, &mut self.values); - tile_set.change_count.set(); + tile_set.change_flag.set(); } fn revert(&mut self, _context: &mut dyn CommandContext) { @@ -504,7 +504,7 @@ impl CommandTrait for RemoveColliderLayerCommand { let uuid = layer.uuid; tile_set.colliders.insert(self.index, layer); tile_set.swap_all_values_for_collider(uuid, &mut self.values); - tile_set.change_count.set(); + tile_set.change_flag.set(); } } @@ -527,7 +527,7 @@ impl CommandTrait for RemovePropertyLayerCommand { let uuid = layer.uuid; self.layer = Some(layer); tile_set.swap_all_values_for_property(uuid, &mut self.values); - tile_set.change_count.set(); + tile_set.change_flag.set(); } fn revert(&mut self, _context: &mut dyn CommandContext) { @@ -536,7 +536,7 @@ impl CommandTrait for RemovePropertyLayerCommand { let uuid = layer.uuid; tile_set.properties.insert(self.index, layer); tile_set.swap_all_values_for_property(uuid, &mut self.values); - tile_set.change_count.set(); + tile_set.change_flag.set(); } } @@ -551,7 +551,7 @@ impl SetBrushPageCommand { fn swap(&mut self) { let mut brush = self.brush.data_ref(); swap_hash_map_entry(brush.pages.entry(self.position), &mut self.page); - brush.change_count.set(); + brush.change_flag.set(); } } @@ -582,7 +582,7 @@ impl SetTileSetPageCommand { swap_hash_map_entry(tile_set.pages.entry(self.position), &mut self.page); tile_set.rebuild_transform_sets(); tile_set.rebuild_animations(); - tile_set.change_count.set(); + tile_set.change_flag.set(); } } @@ -636,7 +636,7 @@ impl MoveTileSetPageCommand { std::mem::swap(&mut self.start_offset, &mut self.end_offset); tile_set.rebuild_transform_sets(); tile_set.rebuild_animations(); - tile_set.change_count.set(); + tile_set.change_flag.set(); } } @@ -686,7 +686,7 @@ impl MoveBrushPageCommand { swap_hash_map_entry(brush.pages.entry(*p + self.end_offset), &mut self.data[i]); } std::mem::swap(&mut self.start_offset, &mut self.end_offset); - brush.change_count.set(); + brush.change_flag.set(); } } @@ -743,7 +743,7 @@ impl MoveTileSetTileCommand { std::mem::swap(&mut self.start_offset, &mut self.end_offset); tile_set.rebuild_transform_sets(); tile_set.rebuild_animations(); - tile_set.change_count.set(); + tile_set.change_flag.set(); } } @@ -800,7 +800,7 @@ impl MoveBrushTileCommand { swap_hash_map_entry(page.tiles.entry(*p + self.end_offset), &mut self.data[i]); } std::mem::swap(&mut self.start_offset, &mut self.end_offset); - brush.change_count.set(); + brush.change_flag.set(); } } @@ -904,7 +904,7 @@ impl TransformTilesCommand { .transformed(self.transformation); } self.transformation = self.transformation.inverted(); - tile_set.change_count.set(); + tile_set.change_flag.set(); } } @@ -935,7 +935,7 @@ impl SetBrushTilesCommand { if let Some(page) = brush.pages.get_mut(&self.page) { page.tiles.swap_tiles(&mut self.tiles); } - brush.change_count.set(); + brush.change_flag.set(); } } @@ -963,7 +963,7 @@ impl SetTileSetTilesCommand { fn swap(&mut self) { let mut tile_set = self.tile_set.data_ref(); tile_set.swap(&mut self.tiles); - tile_set.change_count.set(); + tile_set.change_flag.set(); } } @@ -1034,7 +1034,7 @@ impl ModifyAnimationSpeedCommand { return; }; std::mem::swap(&mut self.frame_rate, &mut anim.frame_rate); - tile_set.change_count.set(); + tile_set.change_flag.set(); } } @@ -1069,7 +1069,7 @@ impl ModifyPageTileSizeCommand { return; }; std::mem::swap(&mut self.size, &mut mat.tile_size); - tile_set.change_count.set(); + tile_set.change_flag.set(); } } @@ -1104,7 +1104,7 @@ impl ModifyPageMaterialCommand { return; }; std::mem::swap(&mut self.material, &mut mat.material); - tile_set.change_count.set(); + tile_set.change_flag.set(); } } @@ -1137,7 +1137,7 @@ impl ModifyPageIconCommand { return; }; std::mem::swap(&mut self.icon, &mut page.icon); - tile_set.change_count.set(); + tile_set.change_flag.set(); } } @@ -1170,7 +1170,7 @@ impl ModifyBrushPageIconCommand { return; }; std::mem::swap(&mut self.icon, &mut page.icon); - brush.change_count.set(); + brush.change_flag.set(); } } @@ -1198,7 +1198,7 @@ impl ModifyBrushTileSetCommand { fn swap(&mut self) { let mut brush = self.brush.data_ref(); std::mem::swap(&mut self.tile_set, &mut brush.tile_set); - brush.change_count.set(); + brush.change_flag.set(); } } diff --git a/editor/src/plugins/tilemap/interaction_mode.rs b/editor/src/plugins/tilemap/interaction_mode.rs index 03b398622..d2d222aeb 100644 --- a/editor/src/plugins/tilemap/interaction_mode.rs +++ b/editor/src/plugins/tilemap/interaction_mode.rs @@ -22,15 +22,21 @@ use commands::{MoveMapTileCommand, SetMapTilesCommand}; use fyrox::{ + asset::untyped::UntypedResource, fxhash::FxHashMap, scene::tilemap::{ - tileset::TileSetRef, OptionTileRect, TileCursorEffect, TileEraseEffect, TileMapData, + brush::TileMapBrushResource, + tileset::{OptionTileSet, TileSetRef}, + MacroTilesUpdate, OptionTileRect, TileCursorEffect, TileEraseEffect, TileMapData, TileOverlayEffect, TileSelectionEffect, TileSource, TileUpdateEffect, TilesUpdate, TransTilesUpdate, }, }; -use crate::make_color_material; +use crate::{ + command::{Command, CommandGroup}, + make_color_material, +}; use super::*; @@ -60,6 +66,13 @@ pub struct TileMapInteractionMode { /// tile map control panel, allowing this object to be aware of the chosen tool /// and the selected stamp. state: TileDrawStateRef, + /// List if tools that can be added to a tile map brush to assist with tile map editing. + brush_macro_list: BrushMacroListRef, + /// Temporary space to store tiles that are in the process of being amended by macros. + macro_update: MacroTilesUpdate, + /// A copy of the list of macro instance resources taken from the current brush. + /// This is temporary storage that is used to avoid repeated allocation. + macro_instance_list: Vec<(Uuid, Option)>, /// A copy of the current drawing mode that is made whenever the user /// presses the mouse button. While the actual drawing mode may change /// during a mouse stroke, this value never will, so nothing breaks by changing @@ -92,13 +105,21 @@ pub struct TileMapInteractionMode { } impl TileMapInteractionMode { - pub fn new(tile_map: Handle, state: TileDrawStateRef, sender: MessageSender) -> Self { + pub fn new( + tile_map: Handle, + state: TileDrawStateRef, + brush_macro_list: BrushMacroListRef, + sender: MessageSender, + ) -> Self { let cursor_material = make_color_material(CURSOR_COLOR); let select_material = make_color_material(SELECT_COLOR); let erase_material = make_color_material(ERASE_COLOR); Self { tile_map, state, + brush_macro_list, + macro_update: MacroTilesUpdate::default(), + macro_instance_list: Vec::default(), current_tool: DrawingMode::Pick, click_grid_position: None, current_grid_position: None, @@ -185,7 +206,7 @@ impl TileMapInteractionMode { let tile_set = state.tile_set.as_ref().map(TileSetRef::new); if let Some(mut tile_set) = tile_set { let tile_set = tile_set.as_loaded(); - for (pos, handle) in stamp.iter() { + for (pos, StampElement { handle, .. }) in stamp.iter() { let handle = tile_set .get_transformed_version(stamp.transformation(), *handle) .unwrap_or(*handle); @@ -257,7 +278,138 @@ fn update_select( let Some(tiles) = tiles.as_loaded_ref() else { return; }; - state.update_stamp(tile_map.tile_set().cloned(), |p| tiles.get(p)); + state.update_stamp(None, tile_map.tile_set().cloned(), |p| { + tiles.get(p).map(|t| t.into()) + }); +} + +fn macro_begin( + tile_map_context: &TileMapContext, + update: &mut TransTilesUpdate, + macro_update: &mut MacroTilesUpdate, + brush: &TileMapBrushResource, + macros: &mut BrushMacroList, + macro_instances: &mut Vec<(Uuid, Option)>, +) { + let tile_map_handle = tile_map_context.node; + let Some(tile_map) = tile_map_context.engine.scenes[tile_map_context.scene].graph + [tile_map_handle] + .cast::() + else { + return; + }; + let brush_guard = brush.data_ref(); + macro_instances.clear(); + macro_instances.extend( + brush_guard + .macros + .iter() + .map(|m| (m.macro_id, m.settings.clone())), + ); + if macro_instances.is_empty() { + return; + } + let tile_set = brush_guard.tile_set(); + let mut tile_set_guard = tile_set.as_ref().map(|ts| ts.data_ref()); + let tile_set = OptionTileSet(tile_set_guard.as_deref_mut()); + update.fill_macro_tiles_update(&tile_set, macro_update); + drop(tile_set_guard); + drop(brush_guard); + let mut context = BrushMacroInstance { + brush: brush.clone(), + settings: None, + }; + for (id, settings) in macro_instances.drain(..) { + let Some(brush_macro) = macros.get_by_uuid_mut(&id) else { + continue; + }; + context.settings = settings; + brush_macro.begin_update(&context, tile_map_context); + brush_macro.amend_update(&context, macro_update, tile_map); + } + macro_update.fill_trans_tiles_update(update); +} + +fn macro_amend_update( + tile_map: &TileMap, + update: &mut TransTilesUpdate, + macro_update: &mut MacroTilesUpdate, + brush: &TileMapBrushResource, + macros: &mut BrushMacroList, + macro_instances: &mut Vec<(Uuid, Option)>, +) { + let brush_guard = brush.data_ref(); + macro_instances.clear(); + macro_instances.extend( + brush_guard + .macros + .iter() + .map(|m| (m.macro_id, m.settings.clone())), + ); + if macro_instances.is_empty() { + return; + } + let tile_set = brush_guard.tile_set(); + let mut tile_set_guard = tile_set.as_ref().map(|ts| ts.data_ref()); + let tile_set = OptionTileSet(tile_set_guard.as_deref_mut()); + update.fill_macro_tiles_update(&tile_set, macro_update); + drop(tile_set_guard); + drop(brush_guard); + let mut context = BrushMacroInstance { + brush: brush.clone(), + settings: None, + }; + for (id, settings) in macro_instances.drain(..) { + let Some(brush_macro) = macros.get_by_uuid_mut(&id) else { + continue; + }; + context.settings = settings; + brush_macro.amend_update(&context, macro_update, tile_map); + } + macro_update.fill_trans_tiles_update(update); +} + +fn macro_command_list( + tile_map: &TileMapContext, + update: &mut TransTilesUpdate, + macro_update: &mut MacroTilesUpdate, + brush: &TileMapBrushResource, + macros: &mut BrushMacroList, + macro_instances: &mut Vec<(Uuid, Option)>, +) -> Vec { + let brush_guard = brush.data_ref(); + macro_instances.clear(); + macro_instances.extend( + brush_guard + .macros + .iter() + .map(|m| (m.macro_id, m.settings.clone())), + ); + if macro_instances.is_empty() { + return Vec::default(); + } + let tile_set = brush_guard.tile_set(); + let mut tile_set_guard = tile_set.as_ref().map(|ts| ts.data_ref()); + let tile_set = OptionTileSet(tile_set_guard.as_deref_mut()); + update.fill_macro_tiles_update(&tile_set, macro_update); + drop(tile_set_guard); + drop(brush_guard); + let mut context = BrushMacroInstance { + brush: brush.clone(), + settings: None, + }; + let mut commands = Vec::default(); + for (id, settings) in macro_instances.drain(..) { + let Some(brush_macro) = macros.get_by_uuid_mut(&id) else { + continue; + }; + context.settings = settings; + if let Some(command) = brush_macro.create_command(&context, macro_update, tile_map) { + commands.push(command); + } + } + macro_update.fill_trans_tiles_update(update); + commands } fn draw( @@ -327,7 +479,8 @@ impl InteractionMode for TileMapInteractionMode { let Some(game_scene) = controller.downcast_mut::() else { return; }; - let scene = &mut engine.scenes[game_scene.scene]; + let scene_handle = game_scene.scene; + let scene = &mut engine.scenes[scene_handle]; let mods = engine.user_interfaces.first().keyboard_modifiers(); let state = self.state.lock(); self.current_tool = state.drawing_mode; @@ -389,14 +542,24 @@ impl InteractionMode for TileMapInteractionMode { } mode => { self.mouse_mode = MouseMode::Drawing; - draw( - &mut self.update_effect.lock().update, - tiles, - mode, - &state, - grid_coord, - grid_coord, - ); + let update = &mut self.update_effect.lock().update; + draw(update, tiles, mode, &state, grid_coord, grid_coord); + drop(tiles_guard); + if let Some(brush) = state.stamp.brush() { + let tile_map = TileMapContext { + node: self.tile_map, + scene: scene_handle, + engine, + }; + macro_begin( + &tile_map, + update, + &mut self.macro_update, + brush, + &mut self.brush_macro_list.lock(), + &mut self.macro_instance_list, + ); + } } } } @@ -414,7 +577,8 @@ impl InteractionMode for TileMapInteractionMode { let Some(game_scene) = controller.downcast_mut::() else { return; }; - let scene = &mut engine.scenes[game_scene.scene]; + let scene_handle = game_scene.scene; + let scene = &mut engine.scenes[scene_handle]; let Some(tile_map) = scene.graph.try_get_mut_of_type::(self.tile_map) else { return; }; @@ -454,14 +618,41 @@ impl InteractionMode for TileMapInteractionMode { if let DrawingMode::Pick = self.current_tool { self.selecting .clone_from(&self.select_effect.lock().positions); - } else if let Some(tile_set) = state.tile_set.as_ref().or(tile_map.tile_set()) { + } else if let Some(tile_set) = + state.tile_set.as_ref().or(tile_map.tile_set()).cloned() + { let update_source = &mut self.update_effect.lock().update; + let tile_map_context = TileMapContext { + node: tile_map_handle, + scene: scene_handle, + engine, + }; + let mut commands = if let Some(brush) = state.stamp.brush() { + macro_command_list( + &tile_map_context, + update_source, + &mut self.macro_update, + brush, + &mut self.brush_macro_list.lock(), + &mut self.macro_instance_list, + ) + } else { + Vec::default() + }; let update = - update_source.build_tiles_update(&TileSetRef::new(tile_set).as_loaded()); - self.sender.do_command(SetMapTilesCommand { + update_source.build_tiles_update(&TileSetRef::new(&tile_set).as_loaded()); + let command = SetMapTilesCommand { tile_map: tile_map_handle, tiles: update, - }); + }; + if !commands.is_empty() { + commands.push(Command::new(command)); + self.sender.do_command( + CommandGroup::from(commands).with_custom_name("Draw Tiles"), + ); + } else { + self.sender.do_command(command); + } update_source.clear(); } } @@ -544,14 +735,19 @@ impl InteractionMode for TileMapInteractionMode { end, ); } else { - draw( - &mut self.update_effect.lock().update, - tiles, - self.current_tool, - &state, - start, - end, - ); + let update = &mut self.update_effect.lock().update; + draw(update, tiles, self.current_tool, &state, start, end); + drop(tiles_guard); + if let Some(brush) = state.stamp.brush() { + macro_amend_update( + tile_map, + update, + &mut self.macro_update, + brush, + &mut self.brush_macro_list.lock(), + &mut self.macro_instance_list, + ); + } } } } diff --git a/editor/src/plugins/tilemap/macro_inspector.rs b/editor/src/plugins/tilemap/macro_inspector.rs new file mode 100644 index 000000000..7f184debb --- /dev/null +++ b/editor/src/plugins/tilemap/macro_inspector.rs @@ -0,0 +1,435 @@ +// Copyright (c) 2019-present Dmitry Stepanov and Fyrox Engine contributors. +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in all +// copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +// SOFTWARE. + +use fyrox::{ + asset::untyped::UntypedResource, + gui::{ + button::{Button, ButtonMessage}, + grid::{Column, GridBuilder, Row}, + stack_panel::StackPanelBuilder, + text::{TextBuilder, TextMessage}, + VerticalAlignment, + }, + scene::tilemap::brush::TileMapBrushResource, +}; + +use super::*; + +const ADD_BUTTON_LABEL: &str = "+"; +const REMOVE_BUTTON_LABEL: &str = "-"; +const ADD_TOOLTIP: &str = "Add the selected cell to this macro."; +const REMOVE_TOOLTIP: &str = "Remove the selected cell from this macro."; +const CELL_WITH_MACRO_COLOR: Color = Color::DARK_SLATE_BLUE; +const CELL_WITHOUT_MACRO_COLOR: Color = Color::opaque(50, 50, 50); + +#[derive(Visit, Reflect)] +pub struct MacroInspector { + handle: Handle, + content: Handle, + #[visit(skip)] + #[reflect(hidden)] + instance_list: Vec>, + #[visit(skip)] + #[reflect(hidden)] + macro_list: BrushMacroListRef, + #[visit(skip)] + #[reflect(hidden)] + cell_sets: MacroCellSetListRef, + #[visit(skip)] + #[reflect(hidden)] + items: Vec, +} + +impl Debug for MacroInspector { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + f.debug_struct("MacroInspector") + .field("handle", &self.handle) + .field("content", &self.content) + .finish() + } +} + +struct Item { + macro_id: Uuid, + editor: Option, +} + +struct ItemEditor { + handle: Handle, + header: ItemHeader, +} + +struct ItemHeader { + handle: Handle, + label: Handle, + button: Handle, +} + +impl ItemEditor { + fn new(header: ItemHeader, content: Handle, ctx: &mut BuildContext) -> Self { + let handle = if content.is_some() { + StackPanelBuilder::new( + WidgetBuilder::new() + .with_child(header.handle) + .with_child(content), + ) + .build(ctx) + } else { + header.handle + }; + Self { handle, header } + } + fn button(&self) -> Handle { + self.header.button + } +} + +fn make_button(title: &str, tooltip: &str, ctx: &mut BuildContext) -> Handle { + ButtonBuilder::new( + WidgetBuilder::new() + .on_column(1) + .with_height(24.0) + .with_margin(Thickness::uniform(1.0)) + .with_tooltip(make_simple_tooltip(ctx, tooltip)), + ) + .with_text(title) + .build(ctx) +} + +fn make_add_button(ctx: &mut BuildContext) -> Handle { + make_button(ADD_BUTTON_LABEL, ADD_TOOLTIP, ctx) +} + +fn make_remove_button(ctx: &mut BuildContext) -> Handle { + make_button(REMOVE_BUTTON_LABEL, REMOVE_TOOLTIP, ctx) +} + +impl ItemHeader { + fn new(name: String, has_cell: bool, ctx: &mut BuildContext) -> Self { + let label = TextBuilder::new( + WidgetBuilder::new().with_vertical_alignment(VerticalAlignment::Center), + ) + .with_vertical_text_alignment(VerticalAlignment::Center) + .with_text(name) + .build(ctx); + let button = if has_cell { + make_remove_button(ctx) + } else { + make_add_button(ctx) + }; + let grid = GridBuilder::new( + WidgetBuilder::new() + .with_child(label) + .with_child(button) + .with_margin(Thickness::uniform(2.0)), + ) + .add_row(Row::auto()) + .add_column(Column::stretch()) + .add_column(Column::strict(20.0)) + .build(ctx); + let back = Brush::Solid(if has_cell { + CELL_WITH_MACRO_COLOR + } else { + CELL_WITHOUT_MACRO_COLOR + }); + Self { + handle: BorderBuilder::new( + WidgetBuilder::new() + .with_background(back.into()) + .with_foreground(Brush::Solid(Color::BLACK).into()) + .with_child(grid), + ) + .build(ctx), + label, + button, + } + } + fn sync(&self, name: String, has_cell: bool, ui: &mut UserInterface) { + ui.send_message(TextMessage::text( + self.label, + MessageDirection::ToWidget, + name, + )); + let button_text = if has_cell { + REMOVE_BUTTON_LABEL + } else { + ADD_BUTTON_LABEL + }; + let button = ui.node(self.button).cast::