From 78848617c0944cac83a386b90cadec74eede2f6a Mon Sep 17 00:00:00 2001 From: Dmitry Stepanov Date: Mon, 27 Jan 2025 20:31:16 +0300 Subject: [PATCH 1/8] texture region editor for nine patch widget - draft --- fyrox-ui/src/inspector/editors/mod.rs | 4 +- .../src/inspector/editors/texture_slice.rs | 381 ++++++++++++++++++ fyrox-ui/src/nine_patch.rs | 54 +-- 3 files changed, 414 insertions(+), 25 deletions(-) create mode 100644 fyrox-ui/src/inspector/editors/texture_slice.rs diff --git a/fyrox-ui/src/inspector/editors/mod.rs b/fyrox-ui/src/inspector/editors/mod.rs index 1f460cb05..bed1d83c9 100644 --- a/fyrox-ui/src/inspector/editors/mod.rs +++ b/fyrox-ui/src/inspector/editors/mod.rs @@ -21,7 +21,6 @@ //! A collection of [PropertyEditorDefinition] objects for a wide variety of types, //! including standard Rust types and Fyrox core types. -use crate::nine_patch::StretchMode; use crate::{ absm::{EventAction, EventKind}, bit::BitField, @@ -82,7 +81,7 @@ use crate::{ menu::{Menu, MenuItem}, message::{CursorIcon, UiMessage}, messagebox::MessageBox, - nine_patch::NinePatch, + nine_patch::{NinePatch, StretchMode}, numeric::NumericUpDown, path::PathEditor, popup::Popup, @@ -141,6 +140,7 @@ pub mod rect; pub mod refcell; pub mod string; mod style; +pub mod texture_slice; pub mod utf32; pub mod uuid; pub mod vec; diff --git a/fyrox-ui/src/inspector/editors/texture_slice.rs b/fyrox-ui/src/inspector/editors/texture_slice.rs new file mode 100644 index 000000000..fb92e7f85 --- /dev/null +++ b/fyrox-ui/src/inspector/editors/texture_slice.rs @@ -0,0 +1,381 @@ +// 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 crate::{ + core::{ + algebra::Vector2, math::Rect, pool::Handle, reflect::prelude::*, some_or_return, + type_traits::prelude::*, visitor::prelude::*, + }, + define_constructor, define_widget_deref, + draw::{CommandTexture, Draw, DrawingContext}, + inspector::{ + editors::{ + PropertyEditorBuildContext, PropertyEditorDefinition, PropertyEditorInstance, + PropertyEditorMessageContext, PropertyEditorTranslationContext, + }, + InspectorError, PropertyChanged, + }, + message::{MessageDirection, OsEvent, UiMessage}, + nine_patch::TextureSlice, + scroll_viewer::ScrollViewerBuilder, + widget::{Widget, WidgetBuilder}, + window::{Window, WindowBuilder}, + BuildContext, Control, Thickness, UiNode, UserInterface, VerticalAlignment, +}; +use fyrox_texture::TextureKind; +use std::{ + any::TypeId, + ops::{Deref, DerefMut}, + sync::mpsc::Sender, +}; + +#[derive(Debug, Clone, PartialEq)] +pub enum TextureSliceEditorMessage { + Slice(TextureSlice), +} + +impl crate::button::ButtonMessage { + define_constructor!(TextureSliceEditorMessage:Slice => fn slice(TextureSlice), layout: false); +} + +#[derive(Clone, Reflect, Visit, TypeUuidProvider, ComponentProvider, Debug)] +#[type_uuid(id = "bd89b59f-13be-4804-bd9c-ed40cfd48b92")] +pub struct TextureSliceEditor { + widget: Widget, + slice: TextureSlice, + handle_size: f32, +} + +define_widget_deref!(TextureSliceEditor); + +impl Control for TextureSliceEditor { + fn measure_override(&self, ui: &UserInterface, available_size: Vector2) -> Vector2 { + let mut size: Vector2 = self.widget.measure_override(ui, available_size); + + if let Some(texture) = self.slice.texture.as_ref() { + let state = texture.state(); + if let Some(data) = state.data_ref() { + if let TextureKind::Rectangle { width, height } = data.kind() { + let width = width as f32; + let height = height as f32; + if size.x < width { + size.x = width; + } + if size.y < height { + size.y = height; + } + } + } + } + + size + } + + fn draw(&self, drawing_context: &mut DrawingContext) { + let texture = some_or_return!(self.slice.texture.deref().clone()); + drawing_context.commit( + self.clip_bounds(), + self.background(), + CommandTexture::Texture(texture.clone()), + None, + ); + + let state = texture.state(); + let texture_data = some_or_return!(state.data_ref()); + + // Only 2D textures can be used with nine-patch. + let TextureKind::Rectangle { width, height } = texture_data.kind() else { + return; + }; + + let texture_width = width as f32; + let texture_height = height as f32; + + let bounds = self + .slice + .texture_region + .map(|region| Rect { + position: region.position.cast::(), + size: region.size.cast::(), + }) + .unwrap_or_else(|| Rect::new(0.0, 0.0, texture_width, texture_height)); + + let left_margin = *self.slice.left_margin as f32; + let right_margin = *self.slice.right_margin as f32; + let top_margin = *self.slice.top_margin as f32; + let bottom_margin = *self.slice.bottom_margin as f32; + + // Draw nine slices. + drawing_context.push_line( + Vector2::new(bounds.position.x + left_margin, bounds.position.y), + Vector2::new( + bounds.position.x + left_margin, + bounds.position.y + bounds.size.y, + ), + 1.0, + ); + drawing_context.push_line( + Vector2::new( + bounds.position.x + bounds.size.x - right_margin, + bounds.position.y, + ), + Vector2::new( + bounds.position.x + bounds.size.x - right_margin, + bounds.position.y + bounds.size.y, + ), + 1.0, + ); + drawing_context.push_line( + Vector2::new(bounds.position.x, bounds.position.y + top_margin), + Vector2::new( + bounds.position.x + bounds.size.x, + bounds.position.y + top_margin, + ), + 1.0, + ); + drawing_context.push_line( + Vector2::new( + bounds.position.x, + bounds.position.y + bounds.size.y - bottom_margin, + ), + Vector2::new( + bounds.position.x + bounds.size.x, + bounds.position.y + bounds.size.y - bottom_margin, + ), + 1.0, + ); + drawing_context.commit( + self.clip_bounds(), + self.foreground(), + CommandTexture::None, + None, + ); + + // Draw handles. + let half_handle_size = self.handle_size / 2.0; + drawing_context.push_rect_filled( + &Rect::new( + bounds.position.x + left_margin - half_handle_size, + bounds.position.y + top_margin - half_handle_size, + self.handle_size, + self.handle_size, + ), + None, + ); + drawing_context.push_rect_filled( + &Rect::new( + bounds.position.x + bounds.size.x - right_margin - half_handle_size, + bounds.position.y + bounds.size.y - bottom_margin - half_handle_size, + self.handle_size, + self.handle_size, + ), + None, + ); + } + + fn handle_routed_message(&mut self, ui: &mut UserInterface, message: &mut UiMessage) { + self.widget.handle_routed_message(ui, message); + + if let Some(TextureSliceEditorMessage::Slice(slice)) = message.data() { + if message.destination() == self.handle() + && message.direction() == MessageDirection::FromWidget + { + self.slice = slice.clone(); + } + } + } +} + +pub struct TextureSliceEditorBuilder { + widget_builder: WidgetBuilder, + slice: TextureSlice, + handle_size: f32, +} + +impl TextureSliceEditorBuilder { + pub fn new(widget_builder: WidgetBuilder) -> Self { + Self { + widget_builder, + slice: Default::default(), + handle_size: 4.0, + } + } + + pub fn with_texture_slice(mut self, slice: TextureSlice) -> Self { + self.slice = slice; + self + } + + pub fn with_handle_size(mut self, size: f32) -> Self { + self.handle_size = size; + self + } + + pub fn build(self, ctx: &mut BuildContext) -> Handle { + ctx.add_node(UiNode::new(TextureSliceEditor { + widget: self.widget_builder.build(ctx), + slice: self.slice, + handle_size: self.handle_size, + })) + } +} + +#[derive(Clone, Reflect, Visit, TypeUuidProvider, ComponentProvider, Debug)] +#[type_uuid(id = "0293081d-55fd-4aa2-a06e-d53fba1a2617")] +pub struct TextureSliceEditorWindow { + window: Window, + slice_editor: Handle, +} + +impl Deref for TextureSliceEditorWindow { + type Target = Widget; + + fn deref(&self) -> &Self::Target { + &self.window.widget + } +} + +impl DerefMut for TextureSliceEditorWindow { + fn deref_mut(&mut self) -> &mut Self::Target { + &mut self.window.widget + } +} + +impl Control for TextureSliceEditorWindow { + fn on_remove(&self, sender: &Sender) { + self.window.on_remove(sender) + } + + fn measure_override(&self, ui: &UserInterface, available_size: Vector2) -> Vector2 { + self.window.measure_override(ui, available_size) + } + + fn arrange_override(&self, ui: &UserInterface, final_size: Vector2) -> Vector2 { + self.window.arrange_override(ui, final_size) + } + + fn draw(&self, drawing_context: &mut DrawingContext) { + self.window.draw(drawing_context) + } + + fn on_visual_transform_changed(&self) { + self.window.on_visual_transform_changed() + } + + fn post_draw(&self, drawing_context: &mut DrawingContext) { + self.window.post_draw(drawing_context) + } + + fn update(&mut self, dt: f32, ui: &mut UserInterface) { + self.window.update(dt, ui); + } + + fn handle_routed_message(&mut self, ui: &mut UserInterface, message: &mut UiMessage) { + self.window.handle_routed_message(ui, message); + if message.data::().is_some() + && message.direction() == MessageDirection::FromWidget + && message.destination() == self.slice_editor + { + // Re-cast the message. + ui.send_message( + message + .clone() + .with_destination(self.handle) + .with_direction(MessageDirection::FromWidget), + ); + } + } + + fn preview_message(&self, ui: &UserInterface, message: &mut UiMessage) { + self.window.preview_message(ui, message); + } + + fn handle_os_event( + &mut self, + self_handle: Handle, + ui: &mut UserInterface, + event: &OsEvent, + ) { + self.window.handle_os_event(self_handle, ui, event); + } +} + +pub struct TextureSliceEditorWindowBuilder { + window_builder: WindowBuilder, +} + +impl TextureSliceEditorWindowBuilder { + pub fn new(window_builder: WindowBuilder) -> Self { + Self { window_builder } + } + + pub fn build(self, ctx: &mut BuildContext) -> Handle { + let slice_editor = TextureSliceEditorBuilder::new(WidgetBuilder::new()).build(ctx); + let scroll_viewer = ScrollViewerBuilder::new(WidgetBuilder::new()) + .with_content(slice_editor) + .build(ctx); + + let node = UiNode::new(TextureSliceEditorWindow { + window: self + .window_builder + .with_content(scroll_viewer) + .build_window(ctx), + slice_editor, + }); + + ctx.add_node(node) + } +} + +#[derive(Debug)] +pub struct TextureSlicePropertyEditorDefinition; + +impl PropertyEditorDefinition for TextureSlicePropertyEditorDefinition { + fn value_type_id(&self) -> TypeId { + TypeId::of::() + } + + fn create_instance( + &self, + ctx: PropertyEditorBuildContext, + ) -> Result { + let value = ctx.property_info.cast_value::()?; + Ok(PropertyEditorInstance::Simple { + editor: TextureSliceEditorBuilder::new( + WidgetBuilder::new() + .with_margin(Thickness::top_bottom(1.0)) + .with_vertical_alignment(VerticalAlignment::Center), + ) + .with_texture_slice(value.clone()) + .build(ctx.build_context), + }) + } + + fn create_message( + &self, + _ctx: PropertyEditorMessageContext, + ) -> Result, InspectorError> { + todo!() + } + + fn translate_message(&self, _ctx: PropertyEditorTranslationContext) -> Option { + todo!() + } +} diff --git a/fyrox-ui/src/nine_patch.rs b/fyrox-ui/src/nine_patch.rs index d88f6d5fb..7388b9cc4 100644 --- a/fyrox-ui/src/nine_patch.rs +++ b/fyrox-ui/src/nine_patch.rs @@ -60,16 +60,21 @@ pub enum StretchMode { Tile, } -#[derive(Default, Clone, Visit, Reflect, Debug, ComponentProvider, TypeUuidProvider)] -#[type_uuid(id = "c345033e-8c10-4186-b101-43f73b85981d")] -pub struct NinePatch { - pub widget: Widget, +#[derive(Default, Clone, Visit, Reflect, Debug, PartialEq)] +pub struct TextureSlice { pub texture: InheritableVariable>, pub bottom_margin: InheritableVariable, pub left_margin: InheritableVariable, pub right_margin: InheritableVariable, pub top_margin: InheritableVariable, pub texture_region: InheritableVariable>>, +} + +#[derive(Default, Clone, Visit, Reflect, Debug, ComponentProvider, TypeUuidProvider)] +#[type_uuid(id = "c345033e-8c10-4186-b101-43f73b85981d")] +pub struct NinePatch { + pub widget: Widget, + pub texture_slice: TextureSlice, pub draw_center: InheritableVariable, pub stretch_mode: InheritableVariable, } @@ -146,11 +151,11 @@ impl Control for NinePatch { fn measure_override(&self, ui: &UserInterface, available_size: Vector2) -> Vector2 { let mut size: Vector2 = available_size; - let column1_width_pixels = *self.left_margin as f32; - let column3_width_pixels = *self.right_margin as f32; + let column1_width_pixels = *self.texture_slice.left_margin as f32; + let column3_width_pixels = *self.texture_slice.right_margin as f32; - let row1_height_pixels = *self.top_margin as f32; - let row3_height_pixels = *self.bottom_margin as f32; + let row1_height_pixels = *self.texture_slice.top_margin as f32; + let row3_height_pixels = *self.texture_slice.bottom_margin as f32; let x_overflow = column1_width_pixels + column3_width_pixels; let y_overflow = row1_height_pixels + row3_height_pixels; @@ -168,11 +173,11 @@ impl Control for NinePatch { } fn arrange_override(&self, ui: &UserInterface, final_size: Vector2) -> Vector2 { - let column1_width_pixels = *self.left_margin as f32; - let column3_width_pixels = *self.right_margin as f32; + let column1_width_pixels = *self.texture_slice.left_margin as f32; + let column3_width_pixels = *self.texture_slice.right_margin as f32; - let row1_height_pixels = *self.top_margin as f32; - let row3_height_pixels = *self.bottom_margin as f32; + let row1_height_pixels = *self.texture_slice.top_margin as f32; + let row3_height_pixels = *self.texture_slice.bottom_margin as f32; let x_overflow = column1_width_pixels + column3_width_pixels; let y_overflow = row1_height_pixels + row3_height_pixels; @@ -192,7 +197,7 @@ impl Control for NinePatch { } fn draw(&self, drawing_context: &mut DrawingContext) { - let texture = some_or_return!(self.texture.as_ref()); + let texture = some_or_return!(self.texture_slice.texture.as_ref()); let texture_state = texture.state(); let texture_state = some_or_return!(texture_state.data_ref()); @@ -207,12 +212,13 @@ impl Control for NinePatch { let patch_bounds = self.widget.bounding_rect(); - let left_margin = *self.left_margin as f32; - let right_margin = *self.right_margin as f32; - let top_margin = *self.top_margin as f32; - let bottom_margin = *self.bottom_margin as f32; + let left_margin = *self.texture_slice.left_margin as f32; + let right_margin = *self.texture_slice.right_margin as f32; + let top_margin = *self.texture_slice.top_margin as f32; + let bottom_margin = *self.texture_slice.bottom_margin as f32; let region = self + .texture_slice .texture_region .map(|region| Rect { position: region.position.cast::(), @@ -486,12 +492,14 @@ impl NinePatchBuilder { ctx.add_node(UiNode::new(NinePatch { widget: self.widget_builder.build(ctx), - texture: self.texture.into(), - bottom_margin: self.bottom_margin.into(), - left_margin: self.left_margin.into(), - right_margin: self.right_margin.into(), - top_margin: self.top_margin.into(), - texture_region: self.texture_region.into(), + texture_slice: TextureSlice { + texture: self.texture.into(), + bottom_margin: self.bottom_margin.into(), + left_margin: self.left_margin.into(), + right_margin: self.right_margin.into(), + top_margin: self.top_margin.into(), + texture_region: self.texture_region.into(), + }, draw_center: self.draw_center.into(), stretch_mode: self.stretch_mode.into(), })) From 91c2e376c0fc1fd6b9676e2d27910c5544fb213f Mon Sep 17 00:00:00 2001 From: Dmitry Stepanov Date: Mon, 27 Jan 2025 23:22:10 +0300 Subject: [PATCH 2/8] more improvements for texture slice editor --- fyrox-ui/src/inspector/editors/mod.rs | 3 + .../src/inspector/editors/texture_slice.rs | 96 +++++++++++++++++-- 2 files changed, 93 insertions(+), 6 deletions(-) diff --git a/fyrox-ui/src/inspector/editors/mod.rs b/fyrox-ui/src/inspector/editors/mod.rs index bed1d83c9..100d22e07 100644 --- a/fyrox-ui/src/inspector/editors/mod.rs +++ b/fyrox-ui/src/inspector/editors/mod.rs @@ -21,6 +21,7 @@ //! A collection of [PropertyEditorDefinition] objects for a wide variety of types, //! including standard Rust types and Fyrox core types. +use crate::inspector::editors::texture_slice::TextureSlicePropertyEditorDefinition; use crate::{ absm::{EventAction, EventKind}, bit::BitField, @@ -586,6 +587,8 @@ impl PropertyEditorDefinitionContainer { Arc>, >::new()); + container.insert(TextureSlicePropertyEditorDefinition); + // Styled. container.insert(InheritablePropertyEditorDefinition::>::new()); container.insert(StyledPropertyEditorDefinition::::new()); diff --git a/fyrox-ui/src/inspector/editors/texture_slice.rs b/fyrox-ui/src/inspector/editors/texture_slice.rs index fb92e7f85..442fa653a 100644 --- a/fyrox-ui/src/inspector/editors/texture_slice.rs +++ b/fyrox-ui/src/inspector/editors/texture_slice.rs @@ -19,6 +19,7 @@ // SOFTWARE. use crate::{ + button::{ButtonBuilder, ButtonMessage}, core::{ algebra::Vector2, math::Rect, pool::Handle, reflect::prelude::*, some_or_return, type_traits::prelude::*, visitor::prelude::*, @@ -36,7 +37,7 @@ use crate::{ nine_patch::TextureSlice, scroll_viewer::ScrollViewerBuilder, widget::{Widget, WidgetBuilder}, - window::{Window, WindowBuilder}, + window::{Window, WindowBuilder, WindowMessage, WindowTitle}, BuildContext, Control, Thickness, UiNode, UserInterface, VerticalAlignment, }; use fyrox_texture::TextureKind; @@ -319,15 +320,26 @@ impl Control for TextureSliceEditorWindow { pub struct TextureSliceEditorWindowBuilder { window_builder: WindowBuilder, + texture_slice: TextureSlice, } impl TextureSliceEditorWindowBuilder { pub fn new(window_builder: WindowBuilder) -> Self { - Self { window_builder } + Self { + window_builder, + texture_slice: Default::default(), + } + } + + pub fn with_texture_slice(mut self, slice: TextureSlice) -> Self { + self.texture_slice = slice; + self } pub fn build(self, ctx: &mut BuildContext) -> Handle { - let slice_editor = TextureSliceEditorBuilder::new(WidgetBuilder::new()).build(ctx); + let slice_editor = TextureSliceEditorBuilder::new(WidgetBuilder::new()) + .with_texture_slice(self.texture_slice) + .build(ctx); let scroll_viewer = ScrollViewerBuilder::new(WidgetBuilder::new()) .with_content(slice_editor) .build(ctx); @@ -344,6 +356,76 @@ impl TextureSliceEditorWindowBuilder { } } +#[derive(Clone, Reflect, Visit, TypeUuidProvider, ComponentProvider, Debug)] +#[type_uuid(id = "024f3a3a-6784-4675-bd99-a4c6c19a8d91")] +pub struct TextureSliceFieldEditor { + widget: Widget, + texture_slice: TextureSlice, + edit: Handle, + editor: Handle, +} + +define_widget_deref!(TextureSliceFieldEditor); + +impl Control for TextureSliceFieldEditor { + fn handle_routed_message(&mut self, ui: &mut UserInterface, message: &mut UiMessage) { + self.widget.handle_routed_message(ui, message); + + if let Some(ButtonMessage::Click) = message.data() { + if message.destination() == self.edit { + self.editor = TextureSliceEditorWindowBuilder::new( + WindowBuilder::new(WidgetBuilder::new().with_width(400.0).with_height(400.0)) + .with_title(WindowTitle::text("Texture Slice Editor")) + .open(false) + .with_remove_on_close(true), + ) + .with_texture_slice(self.texture_slice.clone()) + .build(&mut ui.build_ctx()); + + ui.send_message(WindowMessage::open_modal( + self.editor, + MessageDirection::ToWidget, + true, + true, + )); + } + } + } +} + +pub struct TextureSliceFieldEditorBuilder { + widget_builder: WidgetBuilder, + texture_slice: TextureSlice, +} + +impl TextureSliceFieldEditorBuilder { + pub fn new(widget_builder: WidgetBuilder) -> Self { + Self { + widget_builder, + texture_slice: Default::default(), + } + } + + pub fn with_texture_slice(mut self, slice: TextureSlice) -> Self { + self.texture_slice = slice; + self + } + + pub fn build(self, ctx: &mut BuildContext) -> Handle { + let edit = ButtonBuilder::new(WidgetBuilder::new()) + .with_text("Edit...") + .build(ctx); + + let node = UiNode::new(TextureSliceFieldEditor { + widget: self.widget_builder.with_child(edit).build(ctx), + texture_slice: self.texture_slice, + edit, + editor: Default::default(), + }); + ctx.add_node(node) + } +} + #[derive(Debug)] pub struct TextureSlicePropertyEditorDefinition; @@ -358,7 +440,7 @@ impl PropertyEditorDefinition for TextureSlicePropertyEditorDefinition { ) -> Result { let value = ctx.property_info.cast_value::()?; Ok(PropertyEditorInstance::Simple { - editor: TextureSliceEditorBuilder::new( + editor: TextureSliceFieldEditorBuilder::new( WidgetBuilder::new() .with_margin(Thickness::top_bottom(1.0)) .with_vertical_alignment(VerticalAlignment::Center), @@ -372,10 +454,12 @@ impl PropertyEditorDefinition for TextureSlicePropertyEditorDefinition { &self, _ctx: PropertyEditorMessageContext, ) -> Result, InspectorError> { - todo!() + // TODO + Ok(None) } fn translate_message(&self, _ctx: PropertyEditorTranslationContext) -> Option { - todo!() + // TODO + None } } From 9caf567430c8676f6f32db1685a6a7e7f64e4c52 Mon Sep 17 00:00:00 2001 From: Dmitry Stepanov Date: Tue, 28 Jan 2025 15:08:04 +0300 Subject: [PATCH 3/8] basic functionality of texture slice editor - split texture field in the nine patch widget --- .../src/inspector/editors/texture_slice.rs | 283 +++++++++++++++--- fyrox-ui/src/nine_patch.rs | 42 ++- 2 files changed, 264 insertions(+), 61 deletions(-) diff --git a/fyrox-ui/src/inspector/editors/texture_slice.rs b/fyrox-ui/src/inspector/editors/texture_slice.rs index 442fa653a..6f7c3400d 100644 --- a/fyrox-ui/src/inspector/editors/texture_slice.rs +++ b/fyrox-ui/src/inspector/editors/texture_slice.rs @@ -19,23 +19,29 @@ // SOFTWARE. use crate::{ + brush::Brush, button::{ButtonBuilder, ButtonMessage}, core::{ - algebra::Vector2, math::Rect, pool::Handle, reflect::prelude::*, some_or_return, - type_traits::prelude::*, visitor::prelude::*, + algebra::Vector2, color::Color, math::Rect, pool::Handle, reflect::prelude::*, + some_or_return, type_traits::prelude::*, visitor::prelude::*, }, define_constructor, define_widget_deref, draw::{CommandTexture, Draw, DrawingContext}, + grid::{Column, GridBuilder, Row}, inspector::{ editors::{ PropertyEditorBuildContext, PropertyEditorDefinition, PropertyEditorInstance, PropertyEditorMessageContext, PropertyEditorTranslationContext, }, - InspectorError, PropertyChanged, + FieldKind, InspectorError, PropertyChanged, }, message::{MessageDirection, OsEvent, UiMessage}, nine_patch::TextureSlice, + numeric::{NumericUpDownBuilder, NumericUpDownMessage}, + rect::{RectEditorBuilder, RectEditorMessage}, scroll_viewer::ScrollViewerBuilder, + stack_panel::StackPanelBuilder, + text::TextBuilder, widget::{Widget, WidgetBuilder}, window::{Window, WindowBuilder, WindowMessage, WindowTitle}, BuildContext, Control, Thickness, UiNode, UserInterface, VerticalAlignment, @@ -52,7 +58,7 @@ pub enum TextureSliceEditorMessage { Slice(TextureSlice), } -impl crate::button::ButtonMessage { +impl TextureSliceEditorMessage { define_constructor!(TextureSliceEditorMessage:Slice => fn slice(TextureSlice), layout: false); } @@ -70,7 +76,7 @@ impl Control for TextureSliceEditor { fn measure_override(&self, ui: &UserInterface, available_size: Vector2) -> Vector2 { let mut size: Vector2 = self.widget.measure_override(ui, available_size); - if let Some(texture) = self.slice.texture.as_ref() { + if let Some(texture) = self.slice.texture_source.as_ref() { let state = texture.state(); if let Some(data) = state.data_ref() { if let TextureKind::Rectangle { width, height } = data.kind() { @@ -90,13 +96,7 @@ impl Control for TextureSliceEditor { } fn draw(&self, drawing_context: &mut DrawingContext) { - let texture = some_or_return!(self.slice.texture.deref().clone()); - drawing_context.commit( - self.clip_bounds(), - self.background(), - CommandTexture::Texture(texture.clone()), - None, - ); + let texture = some_or_return!(self.slice.texture_source.clone()); let state = texture.state(); let texture_data = some_or_return!(state.data_ref()); @@ -109,14 +109,23 @@ impl Control for TextureSliceEditor { let texture_width = width as f32; let texture_height = height as f32; - let bounds = self - .slice - .texture_region - .map(|region| Rect { - position: region.position.cast::(), - size: region.size.cast::(), - }) - .unwrap_or_else(|| Rect::new(0.0, 0.0, texture_width, texture_height)); + let mut bounds = Rect { + position: self.slice.texture_region.position.cast::(), + size: self.slice.texture_region.size.cast::(), + }; + + if bounds.size.x == 0.0 && bounds.size.y == 0.0 { + bounds.size.x = texture_width; + bounds.size.y = texture_height; + } + + drawing_context.push_rect_filled(&bounds, None); + drawing_context.commit( + self.clip_bounds(), + self.background(), + CommandTexture::Texture(texture.clone()), + None, + ); let left_margin = *self.slice.left_margin as f32; let right_margin = *self.slice.right_margin as f32; @@ -196,7 +205,7 @@ impl Control for TextureSliceEditor { if let Some(TextureSliceEditorMessage::Slice(slice)) = message.data() { if message.destination() == self.handle() - && message.direction() == MessageDirection::FromWidget + && message.direction() == MessageDirection::ToWidget { self.slice = slice.clone(); } @@ -242,7 +251,14 @@ impl TextureSliceEditorBuilder { #[type_uuid(id = "0293081d-55fd-4aa2-a06e-d53fba1a2617")] pub struct TextureSliceEditorWindow { window: Window, + parent_editor: Handle, slice_editor: Handle, + texture_slice: TextureSlice, + left_margin: Handle, + right_margin: Handle, + top_margin: Handle, + bottom_margin: Handle, + region: Handle, } impl Deref for TextureSliceEditorWindow { @@ -290,17 +306,97 @@ impl Control for TextureSliceEditorWindow { fn handle_routed_message(&mut self, ui: &mut UserInterface, message: &mut UiMessage) { self.window.handle_routed_message(ui, message); - if message.data::().is_some() - && message.direction() == MessageDirection::FromWidget - && message.destination() == self.slice_editor + if let Some(TextureSliceEditorMessage::Slice(slice)) = message.data() { + if message.direction() == MessageDirection::FromWidget + && message.destination() == self.slice_editor + { + // Re-cast the message to self. + ui.send_message( + message + .clone() + .with_destination(self.handle) + .with_direction(MessageDirection::FromWidget), + ); + } + + if message.destination() == self.handle() + && message.direction() == MessageDirection::ToWidget + && &self.texture_slice != slice + { + self.texture_slice = slice.clone(); + + ui.send_message(TextureSliceEditorMessage::slice( + self.slice_editor, + MessageDirection::ToWidget, + self.texture_slice.clone(), + )); + + ui.send_message(RectEditorMessage::value( + self.region, + MessageDirection::ToWidget, + *self.texture_slice.texture_region, + )); + + for (widget, value) in [ + (self.left_margin, &self.texture_slice.left_margin), + (self.right_margin, &self.texture_slice.right_margin), + (self.top_margin, &self.texture_slice.top_margin), + (self.bottom_margin, &self.texture_slice.bottom_margin), + ] { + ui.send_message(NumericUpDownMessage::value( + widget, + MessageDirection::ToWidget, + **value, + )); + } + + // Send the slice to the parent editor. + ui.send_message(TextureSliceEditorMessage::slice( + self.parent_editor, + MessageDirection::ToWidget, + self.texture_slice.clone(), + )); + } + } else if let Some(NumericUpDownMessage::Value(value)) = + message.data::>() { - // Re-cast the message. - ui.send_message( - message - .clone() - .with_destination(self.handle) - .with_direction(MessageDirection::FromWidget), - ); + if message.direction() == MessageDirection::FromWidget { + let mut slice = self.texture_slice.clone(); + let mut target = None; + for (widget, margin) in [ + (self.left_margin, &mut slice.left_margin), + (self.right_margin, &mut slice.right_margin), + (self.top_margin, &mut slice.top_margin), + (self.bottom_margin, &mut slice.bottom_margin), + ] { + if message.destination() == widget { + margin.set_value_and_mark_modified(*value); + target = Some(widget); + break; + } + } + if target.is_some() { + ui.send_message(TextureSliceEditorMessage::slice( + self.handle, + MessageDirection::ToWidget, + slice, + )); + } + } + } else if let Some(RectEditorMessage::Value(value)) = + message.data::>() + { + if message.direction() == MessageDirection::FromWidget + && message.destination() == self.region + { + let mut slice = self.texture_slice.clone(); + slice.texture_region.set_value_and_mark_modified(*value); + ui.send_message(TextureSliceEditorMessage::slice( + self.handle, + MessageDirection::ToWidget, + slice, + )); + } } } @@ -336,20 +432,89 @@ impl TextureSliceEditorWindowBuilder { self } - pub fn build(self, ctx: &mut BuildContext) -> Handle { - let slice_editor = TextureSliceEditorBuilder::new(WidgetBuilder::new()) - .with_texture_slice(self.texture_slice) + pub fn build(self, parent_editor: Handle, ctx: &mut BuildContext) -> Handle { + let region_text = TextBuilder::new(WidgetBuilder::new()) + .with_text("Texture Region") + .build(ctx); + let region = + RectEditorBuilder::new(WidgetBuilder::new().with_margin(Thickness::uniform(1.0))) + .with_value(*self.texture_slice.texture_region) + .build(ctx); + let left_margin_text = TextBuilder::new(WidgetBuilder::new()) + .with_text("Left Margin") + .build(ctx); + let left_margin = + NumericUpDownBuilder::new(WidgetBuilder::new().with_margin(Thickness::uniform(1.0))) + .with_value(*self.texture_slice.left_margin) + .build(ctx); + let right_margin_text = TextBuilder::new(WidgetBuilder::new()) + .with_text("Right Margin") + .build(ctx); + let right_margin = + NumericUpDownBuilder::new(WidgetBuilder::new().with_margin(Thickness::uniform(1.0))) + .with_value(*self.texture_slice.right_margin) + .build(ctx); + let top_margin_text = TextBuilder::new(WidgetBuilder::new()) + .with_text("Top Margin") + .build(ctx); + let top_margin = + NumericUpDownBuilder::new(WidgetBuilder::new().with_margin(Thickness::uniform(1.0))) + .with_value(*self.texture_slice.top_margin) + .build(ctx); + let bottom_margin_text = TextBuilder::new(WidgetBuilder::new()) + .with_text("Bottom Margin") .build(ctx); - let scroll_viewer = ScrollViewerBuilder::new(WidgetBuilder::new()) + let bottom_margin = + NumericUpDownBuilder::new(WidgetBuilder::new().with_margin(Thickness::uniform(1.0))) + .with_value(*self.texture_slice.bottom_margin) + .build(ctx); + + let toolbar = StackPanelBuilder::new( + WidgetBuilder::new() + .with_child(region_text) + .with_child(region) + .with_child(left_margin_text) + .with_child(left_margin) + .with_child(right_margin_text) + .with_child(right_margin) + .with_child(top_margin_text) + .with_child(top_margin) + .with_child(bottom_margin_text) + .with_child(bottom_margin) + .on_column(0), + ) + .build(ctx); + + let slice_editor = TextureSliceEditorBuilder::new( + WidgetBuilder::new() + .with_background(Brush::Solid(Color::WHITE).into()) + .with_foreground(Brush::Solid(Color::GREEN).into()), + ) + .with_texture_slice(self.texture_slice.clone()) + .build(ctx); + let scroll_viewer = ScrollViewerBuilder::new(WidgetBuilder::new().on_column(1)) .with_content(slice_editor) .build(ctx); + let content = GridBuilder::new( + WidgetBuilder::new() + .with_child(toolbar) + .with_child(scroll_viewer), + ) + .add_column(Column::strict(150.0)) + .add_column(Column::stretch()) + .add_row(Row::stretch()) + .build(ctx); let node = UiNode::new(TextureSliceEditorWindow { - window: self - .window_builder - .with_content(scroll_viewer) - .build_window(ctx), + window: self.window_builder.with_content(content).build_window(ctx), + parent_editor, slice_editor, + texture_slice: self.texture_slice, + left_margin, + right_margin, + top_margin, + bottom_margin, + region, }); ctx.add_node(node) @@ -374,13 +539,13 @@ impl Control for TextureSliceFieldEditor { if let Some(ButtonMessage::Click) = message.data() { if message.destination() == self.edit { self.editor = TextureSliceEditorWindowBuilder::new( - WindowBuilder::new(WidgetBuilder::new().with_width(400.0).with_height(400.0)) + WindowBuilder::new(WidgetBuilder::new().with_width(700.0).with_height(500.0)) .with_title(WindowTitle::text("Texture Slice Editor")) .open(false) .with_remove_on_close(true), ) .with_texture_slice(self.texture_slice.clone()) - .build(&mut ui.build_ctx()); + .build(self.handle, &mut ui.build_ctx()); ui.send_message(WindowMessage::open_modal( self.editor, @@ -389,6 +554,20 @@ impl Control for TextureSliceFieldEditor { true, )); } + } else if let Some(TextureSliceEditorMessage::Slice(slice)) = message.data() { + if message.destination() == self.handle + && message.direction() == MessageDirection::ToWidget + && &self.texture_slice != slice + { + self.texture_slice = slice.clone(); + + ui.send_message( + message + .clone() + .with_destination(self.handle) + .with_direction(MessageDirection::FromWidget), + ); + } } } } @@ -452,14 +631,26 @@ impl PropertyEditorDefinition for TextureSlicePropertyEditorDefinition { fn create_message( &self, - _ctx: PropertyEditorMessageContext, + ctx: PropertyEditorMessageContext, ) -> Result, InspectorError> { - // TODO - Ok(None) + let value = ctx.property_info.cast_value::()?; + Ok(Some(TextureSliceEditorMessage::slice( + ctx.instance, + MessageDirection::ToWidget, + value.clone(), + ))) } - fn translate_message(&self, _ctx: PropertyEditorTranslationContext) -> Option { - // TODO + fn translate_message(&self, ctx: PropertyEditorTranslationContext) -> Option { + if ctx.message.direction() == MessageDirection::FromWidget { + if let Some(TextureSliceEditorMessage::Slice(value)) = ctx.message.data() { + return Some(PropertyChanged { + name: ctx.name.to_string(), + owner_type_id: ctx.owner_type_id, + value: FieldKind::object(value.clone()), + }); + } + } None } } diff --git a/fyrox-ui/src/nine_patch.rs b/fyrox-ui/src/nine_patch.rs index 7388b9cc4..ff5b2b39e 100644 --- a/fyrox-ui/src/nine_patch.rs +++ b/fyrox-ui/src/nine_patch.rs @@ -62,12 +62,13 @@ pub enum StretchMode { #[derive(Default, Clone, Visit, Reflect, Debug, PartialEq)] pub struct TextureSlice { - pub texture: InheritableVariable>, + // This field is used only for editing purposes in the UI. + pub texture_source: Option, pub bottom_margin: InheritableVariable, pub left_margin: InheritableVariable, pub right_margin: InheritableVariable, pub top_margin: InheritableVariable, - pub texture_region: InheritableVariable>>, + pub texture_region: InheritableVariable>, } #[derive(Default, Clone, Visit, Reflect, Debug, ComponentProvider, TypeUuidProvider)] @@ -76,9 +77,18 @@ pub struct NinePatch { pub widget: Widget, pub texture_slice: TextureSlice, pub draw_center: InheritableVariable, + #[reflect(setter = "set_texture")] + pub texture: InheritableVariable>, pub stretch_mode: InheritableVariable, } +impl NinePatch { + pub fn set_texture(&mut self, texture: Option) { + self.texture.set_value_and_mark_modified(texture.clone()); + self.texture_slice.texture_source = texture; + } +} + impl ConstructorProvider for NinePatch { fn constructor() -> GraphNodeConstructor { GraphNodeConstructor::new::() @@ -197,7 +207,7 @@ impl Control for NinePatch { } fn draw(&self, drawing_context: &mut DrawingContext) { - let texture = some_or_return!(self.texture_slice.texture.as_ref()); + let texture = some_or_return!(self.texture.as_ref()); let texture_state = texture.state(); let texture_state = some_or_return!(texture_state.data_ref()); @@ -217,14 +227,15 @@ impl Control for NinePatch { let top_margin = *self.texture_slice.top_margin as f32; let bottom_margin = *self.texture_slice.bottom_margin as f32; - let region = self - .texture_slice - .texture_region - .map(|region| Rect { - position: region.position.cast::(), - size: region.size.cast::(), - }) - .unwrap_or_else(|| Rect::new(0.0, 0.0, texture_width, texture_height)); + let mut region = Rect { + position: self.texture_slice.texture_region.position.cast::(), + size: self.texture_slice.texture_region.size.cast::(), + }; + + if region.size.x == 0.0 && region.size.y == 0.0 { + region.size.x = texture_width; + region.size.y = texture_height; + } let center_uv_x_min = (region.position.x + left_margin) / texture_width; let center_uv_x_max = (region.position.x + region.size.x - right_margin) / texture_width; @@ -425,7 +436,7 @@ pub struct NinePatchBuilder { pub left_margin: u32, pub right_margin: u32, pub top_margin: u32, - pub texture_region: Option>, + pub texture_region: Rect, pub draw_center: bool, pub stretch_mode: StretchMode, } @@ -439,7 +450,7 @@ impl NinePatchBuilder { left_margin: 0, right_margin: 0, top_margin: 0, - texture_region: None, + texture_region: Default::default(), draw_center: true, stretch_mode: Default::default(), } @@ -471,7 +482,7 @@ impl NinePatchBuilder { } pub fn with_texture_region(mut self, rect: Rect) -> Self { - self.texture_region = Some(rect); + self.texture_region = rect; self } @@ -493,7 +504,7 @@ impl NinePatchBuilder { ctx.add_node(UiNode::new(NinePatch { widget: self.widget_builder.build(ctx), texture_slice: TextureSlice { - texture: self.texture.into(), + texture_source: self.texture.clone(), bottom_margin: self.bottom_margin.into(), left_margin: self.left_margin.into(), right_margin: self.right_margin.into(), @@ -501,6 +512,7 @@ impl NinePatchBuilder { texture_region: self.texture_region.into(), }, draw_center: self.draw_center.into(), + texture: self.texture.into(), stretch_mode: self.stretch_mode.into(), })) } From 8710389cafd7405b551e85b54613e915380332d2 Mon Sep 17 00:00:00 2001 From: Dmitry Stepanov Date: Tue, 28 Jan 2025 16:23:35 +0300 Subject: [PATCH 4/8] improved slice visualization --- .../src/inspector/editors/texture_slice.rs | 24 +++++++++++++++---- 1 file changed, 20 insertions(+), 4 deletions(-) diff --git a/fyrox-ui/src/inspector/editors/texture_slice.rs b/fyrox-ui/src/inspector/editors/texture_slice.rs index 6f7c3400d..d88ba7d62 100644 --- a/fyrox-ui/src/inspector/editors/texture_slice.rs +++ b/fyrox-ui/src/inspector/editors/texture_slice.rs @@ -109,6 +109,14 @@ impl Control for TextureSliceEditor { let texture_width = width as f32; let texture_height = height as f32; + drawing_context.push_rect_filled(&Rect::new(0.0, 0.0, texture_width, texture_height), None); + drawing_context.commit( + self.clip_bounds(), + self.background(), + CommandTexture::Texture(texture.clone()), + None, + ); + let mut bounds = Rect { position: self.slice.texture_region.position.cast::(), size: self.slice.texture_region.size.cast::(), @@ -119,10 +127,10 @@ impl Control for TextureSliceEditor { bounds.size.y = texture_height; } - drawing_context.push_rect_filled(&bounds, None); + drawing_context.push_rect(&bounds, 1.0); drawing_context.commit( self.clip_bounds(), - self.background(), + self.foreground(), CommandTexture::Texture(texture.clone()), None, ); @@ -198,6 +206,12 @@ impl Control for TextureSliceEditor { ), None, ); + drawing_context.commit( + self.clip_bounds(), + self.foreground(), + CommandTexture::None, + None, + ); } fn handle_routed_message(&mut self, ui: &mut UserInterface, message: &mut UiMessage) { @@ -224,7 +238,7 @@ impl TextureSliceEditorBuilder { Self { widget_builder, slice: Default::default(), - handle_size: 4.0, + handle_size: 8.0, } } @@ -487,8 +501,10 @@ impl TextureSliceEditorWindowBuilder { let slice_editor = TextureSliceEditorBuilder::new( WidgetBuilder::new() + .with_clip_to_bounds(false) .with_background(Brush::Solid(Color::WHITE).into()) - .with_foreground(Brush::Solid(Color::GREEN).into()), + .with_foreground(Brush::Solid(Color::GREEN).into()) + .with_margin(Thickness::uniform(3.0)), ) .with_texture_slice(self.texture_slice.clone()) .build(ctx); From fb775d7fc3c2d380a713b2e73603da5d5889549d Mon Sep 17 00:00:00 2001 From: Dmitry Stepanov Date: Tue, 28 Jan 2025 16:36:23 +0300 Subject: [PATCH 5/8] visualize region bounds handles --- .../src/inspector/editors/texture_slice.rs | 36 ++++++++++--------- 1 file changed, 20 insertions(+), 16 deletions(-) diff --git a/fyrox-ui/src/inspector/editors/texture_slice.rs b/fyrox-ui/src/inspector/editors/texture_slice.rs index d88ba7d62..384f4d5e9 100644 --- a/fyrox-ui/src/inspector/editors/texture_slice.rs +++ b/fyrox-ui/src/inspector/editors/texture_slice.rs @@ -188,24 +188,28 @@ impl Control for TextureSliceEditor { // Draw handles. let half_handle_size = self.handle_size / 2.0; - drawing_context.push_rect_filled( - &Rect::new( - bounds.position.x + left_margin - half_handle_size, - bounds.position.y + top_margin - half_handle_size, - self.handle_size, - self.handle_size, + for position in [ + // Margin bounds. + Vector2::new( + bounds.position.x + left_margin, + bounds.position.y + top_margin, ), - None, - ); - drawing_context.push_rect_filled( - &Rect::new( - bounds.position.x + bounds.size.x - right_margin - half_handle_size, - bounds.position.y + bounds.size.y - bottom_margin - half_handle_size, - self.handle_size, - self.handle_size, + Vector2::new( + bounds.position.x + bounds.size.x - right_margin, + bounds.position.y + bounds.size.y - bottom_margin, ), - None, - ); + // Region bounds. + bounds.position, + bounds.right_bottom_corner(), + ] { + drawing_context.push_rect_filled( + &Rect { + position: position - Vector2::repeat(half_handle_size), + size: Vector2::repeat(self.handle_size), + }, + None, + ); + } drawing_context.commit( self.clip_bounds(), self.foreground(), From 63f9ec882ec36b3fa4ee9e590a697c0ab8771185 Mon Sep 17 00:00:00 2001 From: Dmitry Stepanov Date: Tue, 28 Jan 2025 20:52:45 +0300 Subject: [PATCH 6/8] thumb widget for draggable things --- fyrox-ui/src/lib.rs | 1 + fyrox-ui/src/thumb.rs | 142 ++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 143 insertions(+) create mode 100644 fyrox-ui/src/thumb.rs diff --git a/fyrox-ui/src/lib.rs b/fyrox-ui/src/lib.rs index d8a97e799..5360e260b 100644 --- a/fyrox-ui/src/lib.rs +++ b/fyrox-ui/src/lib.rs @@ -259,6 +259,7 @@ pub mod tab_control; pub mod text; pub mod text_box; mod thickness; +pub mod thumb; pub mod toggle; pub mod tree; pub mod utils; diff --git a/fyrox-ui/src/thumb.rs b/fyrox-ui/src/thumb.rs new file mode 100644 index 000000000..a7616f832 --- /dev/null +++ b/fyrox-ui/src/thumb.rs @@ -0,0 +1,142 @@ +// 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 crate::{ + core::{ + algebra::Vector2, pool::Handle, reflect::prelude::*, type_traits::prelude::*, + visitor::prelude::*, + }, + define_constructor, + message::{ButtonState, MessageDirection, MouseButton, UiMessage}, + widget::{Widget, WidgetBuilder, WidgetMessage}, + BuildContext, Control, UiNode, UserInterface, +}; +use fyrox_graph::constructor::{ConstructorProvider, GraphNodeConstructor}; +use std::ops::{Deref, DerefMut}; + +#[derive(Debug, Clone, PartialEq)] +pub enum ThumbMessage { + DragStarted { position: Vector2 }, + DragDelta { offset: Vector2 }, + DragCompleted { position: Vector2 }, +} + +impl ThumbMessage { + define_constructor!(ThumbMessage:DragStarted => fn drag_started(position: Vector2), layout: false); + define_constructor!(ThumbMessage:DragDelta => fn drag_delta(offset: Vector2), layout: false); + define_constructor!(ThumbMessage:DragCompleted => fn drag_completed(position: Vector2), layout: false); +} + +#[derive(Default, Clone, Visit, Reflect, Debug, TypeUuidProvider, ComponentProvider)] +#[type_uuid(id = "71ad2ff4-6e9e-461d-b7c2-867bd4039684")] +pub struct Thumb { + pub widget: Widget, + pub click_pos: Vector2, +} + +impl ConstructorProvider for Thumb { + fn constructor() -> GraphNodeConstructor { + GraphNodeConstructor::new::() + .with_variant("Thumb", |ui| { + ThumbBuilder::new(WidgetBuilder::new().with_name("Thumb")) + .build(&mut ui.build_ctx()) + .into() + }) + .with_group("Input") + } +} + +crate::define_widget_deref!(Thumb); + +impl Control for Thumb { + fn handle_routed_message(&mut self, ui: &mut UserInterface, message: &mut UiMessage) { + self.widget.handle_routed_message(ui, message); + + if let Some(msg) = message.data::() { + match msg { + WidgetMessage::MouseDown { pos, button } => { + if !message.handled() && *button == MouseButton::Left { + ui.capture_mouse(self.handle); + message.set_handled(true); + self.click_pos = *pos; + ui.send_message(ThumbMessage::drag_started( + self.handle, + MessageDirection::FromWidget, + self.actual_local_position(), + )); + } + } + WidgetMessage::MouseUp { button, .. } => { + if ui.captured_node() == self.handle && *button == MouseButton::Left { + ui.send_message(ThumbMessage::drag_completed( + self.handle, + MessageDirection::FromWidget, + self.actual_local_position(), + )); + + ui.release_mouse_capture(); + } + } + WidgetMessage::MouseMove { pos, state } => { + if ui.captured_node() == self.handle && state.left == ButtonState::Pressed { + ui.send_message(ThumbMessage::drag_delta( + self.handle, + MessageDirection::FromWidget, + self.visual_transform() + .try_inverse() + .unwrap_or_default() + .transform_vector(&(*pos - self.click_pos)), + )); + } + } + _ => (), + } + } + } +} + +pub struct ThumbBuilder { + widget_builder: WidgetBuilder, +} + +impl ThumbBuilder { + pub fn new(widget_builder: WidgetBuilder) -> Self { + Self { widget_builder } + } + + pub fn build(self, ctx: &mut BuildContext) -> Handle { + let thumb = Thumb { + widget: self.widget_builder.build(ctx), + click_pos: Default::default(), + }; + ctx.add_node(UiNode::new(thumb)) + } +} + +#[cfg(test)] +mod test { + use crate::thumb::ThumbBuilder; + use crate::{test::test_widget_deletion, widget::WidgetBuilder}; + + #[test] + fn test_deletion() { + test_widget_deletion(|ctx| ThumbBuilder::new(WidgetBuilder::new()).build(ctx)); + } +} From 0310316c41d275b44bad30c8e29e3cff18cc1e01 Mon Sep 17 00:00:00 2001 From: Dmitry Stepanov Date: Tue, 28 Jan 2025 21:14:44 +0300 Subject: [PATCH 7/8] ability to edit texture slice using corner thumbs --- .../src/inspector/editors/texture_slice.rs | 195 ++++++++++++++---- fyrox-ui/src/nine_patch.rs | 28 +++ 2 files changed, 187 insertions(+), 36 deletions(-) diff --git a/fyrox-ui/src/inspector/editors/texture_slice.rs b/fyrox-ui/src/inspector/editors/texture_slice.rs index 384f4d5e9..9c09372d8 100644 --- a/fyrox-ui/src/inspector/editors/texture_slice.rs +++ b/fyrox-ui/src/inspector/editors/texture_slice.rs @@ -19,12 +19,14 @@ // SOFTWARE. use crate::{ + border::BorderBuilder, brush::Brush, button::{ButtonBuilder, ButtonMessage}, core::{ algebra::Vector2, color::Color, math::Rect, pool::Handle, reflect::prelude::*, some_or_return, type_traits::prelude::*, visitor::prelude::*, }, + decorator::DecoratorBuilder, define_constructor, define_widget_deref, draw::{CommandTexture, Draw, DrawingContext}, grid::{Column, GridBuilder, Row}, @@ -35,14 +37,15 @@ use crate::{ }, FieldKind, InspectorError, PropertyChanged, }, - message::{MessageDirection, OsEvent, UiMessage}, + message::{CursorIcon, MessageDirection, OsEvent, UiMessage}, nine_patch::TextureSlice, numeric::{NumericUpDownBuilder, NumericUpDownMessage}, rect::{RectEditorBuilder, RectEditorMessage}, scroll_viewer::ScrollViewerBuilder, stack_panel::StackPanelBuilder, text::TextBuilder, - widget::{Widget, WidgetBuilder}, + thumb::{ThumbBuilder, ThumbMessage}, + widget::{Widget, WidgetBuilder, WidgetMessage}, window::{Window, WindowBuilder, WindowMessage, WindowTitle}, BuildContext, Control, Thickness, UiNode, UserInterface, VerticalAlignment, }; @@ -62,12 +65,29 @@ impl TextureSliceEditorMessage { define_constructor!(TextureSliceEditorMessage:Slice => fn slice(TextureSlice), layout: false); } +#[derive(Debug, Clone, PartialEq)] +struct DragContext { + initial_position: Vector2, + bottom_margin: u32, + left_margin: u32, + right_margin: u32, + top_margin: u32, + texture_region: Rect, +} + #[derive(Clone, Reflect, Visit, TypeUuidProvider, ComponentProvider, Debug)] #[type_uuid(id = "bd89b59f-13be-4804-bd9c-ed40cfd48b92")] pub struct TextureSliceEditor { widget: Widget, slice: TextureSlice, handle_size: f32, + region_min_thumb: Handle, + region_max_thumb: Handle, + slice_min_thumb: Handle, + slice_max_thumb: Handle, + #[reflect(hidden)] + #[visit(skip)] + drag_context: Option, } define_widget_deref!(TextureSliceEditor); @@ -95,6 +115,23 @@ impl Control for TextureSliceEditor { size } + fn arrange_override(&self, ui: &UserInterface, final_size: Vector2) -> Vector2 { + for &child_handle in self.widget.children() { + let child = ui.nodes.borrow(child_handle); + ui.arrange_node( + child_handle, + &Rect::new( + child.desired_local_position().x, + child.desired_local_position().y, + child.desired_size().x, + child.desired_size().y, + ), + ); + } + + final_size + } + fn draw(&self, drawing_context: &mut DrawingContext) { let texture = some_or_return!(self.slice.texture_source.clone()); @@ -185,37 +222,6 @@ impl Control for TextureSliceEditor { CommandTexture::None, None, ); - - // Draw handles. - let half_handle_size = self.handle_size / 2.0; - for position in [ - // Margin bounds. - Vector2::new( - bounds.position.x + left_margin, - bounds.position.y + top_margin, - ), - Vector2::new( - bounds.position.x + bounds.size.x - right_margin, - bounds.position.y + bounds.size.y - bottom_margin, - ), - // Region bounds. - bounds.position, - bounds.right_bottom_corner(), - ] { - drawing_context.push_rect_filled( - &Rect { - position: position - Vector2::repeat(half_handle_size), - size: Vector2::repeat(self.handle_size), - }, - None, - ); - } - drawing_context.commit( - self.clip_bounds(), - self.foreground(), - CommandTexture::None, - None, - ); } fn handle_routed_message(&mut self, ui: &mut UserInterface, message: &mut UiMessage) { @@ -227,6 +233,80 @@ impl Control for TextureSliceEditor { { self.slice = slice.clone(); } + } else if let Some(msg) = message.data::() { + match msg { + ThumbMessage::DragStarted { position } => { + self.drag_context = Some(DragContext { + initial_position: *position, + bottom_margin: *self.slice.bottom_margin, + left_margin: *self.slice.left_margin, + right_margin: *self.slice.right_margin, + top_margin: *self.slice.top_margin, + texture_region: *self.slice.texture_region, + }); + } + ThumbMessage::DragDelta { offset } => { + if let Some(drag_context) = self.drag_context.as_ref() { + let texture = some_or_return!(self.slice.texture_source.clone()); + let state = texture.state(); + let texture_data = some_or_return!(state.data_ref()); + let TextureKind::Rectangle { width, height } = texture_data.kind() else { + return; + }; + + let texture_width = width as f32; + let texture_height = height as f32; + + let mut new_pos = drag_context.initial_position + *offset; + new_pos.x = new_pos.x.clamp(0.0, texture_width); + new_pos.y = new_pos.y.clamp(0.0, texture_height); + + ui.send_message(WidgetMessage::desired_position( + message.destination(), + MessageDirection::ToWidget, + new_pos, + )); + + if message.destination() == self.slice_min_thumb { + self.slice.top_margin.set_value_and_mark_modified( + (drag_context.top_margin as f32 + offset.y) as u32, + ); + self.slice.left_margin.set_value_and_mark_modified( + (drag_context.left_margin as f32 + offset.x) as u32, + ); + } else if message.destination() == self.slice_max_thumb { + self.slice.bottom_margin.set_value_and_mark_modified( + (drag_context.bottom_margin as f32 + offset.y) as u32, + ); + self.slice.right_margin.set_value_and_mark_modified( + (drag_context.right_margin as f32 + offset.x) as u32, + ); + } else if message.destination() == self.region_min_thumb { + self.slice.texture_region.position = Vector2::new( + (drag_context.texture_region.position.x as f32 + offset.x) as u32, + (drag_context.texture_region.position.y as f32 + offset.y) as u32, + ); + self.slice.texture_region.size = Vector2::new( + (drag_context.texture_region.size.x as f32 - offset.x) as u32, + (drag_context.texture_region.size.y as f32 - offset.y) as u32, + ); + } else if message.destination() == self.region_max_thumb { + self.slice.texture_region.size = Vector2::new( + (drag_context.texture_region.size.x as f32 + offset.x) as u32, + (drag_context.texture_region.size.y as f32 + offset.y) as u32, + ); + } + } + } + ThumbMessage::DragCompleted { .. } => { + self.drag_context = None; + ui.send_message(TextureSliceEditorMessage::slice( + self.handle(), + MessageDirection::FromWidget, + self.slice.clone(), + )); + } + } } } } @@ -237,6 +317,28 @@ pub struct TextureSliceEditorBuilder { handle_size: f32, } +fn make_thumb(position: Vector2, handle_size: f32, ctx: &mut BuildContext) -> Handle { + ThumbBuilder::new( + WidgetBuilder::new() + .with_desired_position(position.cast::()) + .with_child( + DecoratorBuilder::new(BorderBuilder::new( + WidgetBuilder::new() + .with_width(handle_size) + .with_height(handle_size) + .with_cursor(Some(CursorIcon::Grab)) + .with_foreground(Brush::Solid(Color::opaque(0, 150, 0)).into()), + )) + .with_pressable(false) + .with_selected(false) + .with_normal_brush(Brush::Solid(Color::opaque(0, 150, 0)).into()) + .with_hover_brush(Brush::Solid(Color::opaque(0, 255, 0)).into()) + .build(ctx), + ), + ) + .build(ctx) +} + impl TextureSliceEditorBuilder { pub fn new(widget_builder: WidgetBuilder) -> Self { Self { @@ -257,10 +359,31 @@ impl TextureSliceEditorBuilder { } pub fn build(self, ctx: &mut BuildContext) -> Handle { + let region_min_thumb = + make_thumb(self.slice.texture_region.position, self.handle_size, ctx); + let region_max_thumb = make_thumb( + self.slice.texture_region.right_bottom_corner(), + self.handle_size, + ctx, + ); + let slice_min_thumb = make_thumb(self.slice.margin_min(), self.handle_size, ctx); + let slice_max_thumb = make_thumb(self.slice.margin_max(), self.handle_size, ctx); + ctx.add_node(UiNode::new(TextureSliceEditor { - widget: self.widget_builder.build(ctx), + widget: self + .widget_builder + .with_child(region_min_thumb) + .with_child(region_max_thumb) + .with_child(slice_min_thumb) + .with_child(slice_max_thumb) + .build(ctx), slice: self.slice, handle_size: self.handle_size, + region_min_thumb, + region_max_thumb, + slice_min_thumb, + slice_max_thumb, + drag_context: None, })) } } @@ -328,11 +451,11 @@ impl Control for TextureSliceEditorWindow { if message.direction() == MessageDirection::FromWidget && message.destination() == self.slice_editor { - // Re-cast the message to self. + // Re-cast the message to parent editor. ui.send_message( message .clone() - .with_destination(self.handle) + .with_destination(self.parent_editor) .with_direction(MessageDirection::FromWidget), ); } diff --git a/fyrox-ui/src/nine_patch.rs b/fyrox-ui/src/nine_patch.rs index ff5b2b39e..9a8324cd8 100644 --- a/fyrox-ui/src/nine_patch.rs +++ b/fyrox-ui/src/nine_patch.rs @@ -71,6 +71,34 @@ pub struct TextureSlice { pub texture_region: InheritableVariable>, } +impl TextureSlice { + /// Returns the top left point. + pub fn margin_min(&self) -> Vector2 { + Vector2::new( + self.texture_region.position.x + *self.left_margin, + self.texture_region.position.y + *self.top_margin, + ) + } + + /// Returns the bottom right point. + pub fn margin_max(&self) -> Vector2 { + Vector2::new( + self.texture_region.position.x + + self + .texture_region + .size + .x + .saturating_sub(*self.right_margin), + self.texture_region.position.y + + self + .texture_region + .size + .y + .saturating_sub(*self.bottom_margin), + ) + } +} + #[derive(Default, Clone, Visit, Reflect, Debug, ComponentProvider, TypeUuidProvider)] #[type_uuid(id = "c345033e-8c10-4186-b101-43f73b85981d")] pub struct NinePatch { From 7ebba912e1aa8e015b975c22a8c62ee570c7ac61 Mon Sep 17 00:00:00 2001 From: Dmitry Stepanov Date: Tue, 28 Jan 2025 21:29:46 +0300 Subject: [PATCH 8/8] slice editor fixes - fixed dragging of bottom right corner - sync numeric fields after dragging --- .../src/inspector/editors/texture_slice.rs | 69 ++++++++++--------- 1 file changed, 35 insertions(+), 34 deletions(-) diff --git a/fyrox-ui/src/inspector/editors/texture_slice.rs b/fyrox-ui/src/inspector/editors/texture_slice.rs index 9c09372d8..162673033 100644 --- a/fyrox-ui/src/inspector/editors/texture_slice.rs +++ b/fyrox-ui/src/inspector/editors/texture_slice.rs @@ -276,10 +276,10 @@ impl Control for TextureSliceEditor { ); } else if message.destination() == self.slice_max_thumb { self.slice.bottom_margin.set_value_and_mark_modified( - (drag_context.bottom_margin as f32 + offset.y) as u32, + (drag_context.bottom_margin as f32 - offset.y) as u32, ); self.slice.right_margin.set_value_and_mark_modified( - (drag_context.right_margin as f32 + offset.x) as u32, + (drag_context.right_margin as f32 - offset.x) as u32, ); } else if message.destination() == self.region_min_thumb { self.slice.texture_region.position = Vector2::new( @@ -402,6 +402,36 @@ pub struct TextureSliceEditorWindow { region: Handle, } +impl TextureSliceEditorWindow { + fn on_slice_changed(&self, ui: &UserInterface) { + ui.send_message(RectEditorMessage::value( + self.region, + MessageDirection::ToWidget, + *self.texture_slice.texture_region, + )); + + for (widget, value) in [ + (self.left_margin, &self.texture_slice.left_margin), + (self.right_margin, &self.texture_slice.right_margin), + (self.top_margin, &self.texture_slice.top_margin), + (self.bottom_margin, &self.texture_slice.bottom_margin), + ] { + ui.send_message(NumericUpDownMessage::value( + widget, + MessageDirection::ToWidget, + **value, + )); + } + + // Send the slice to the parent editor. + ui.send_message(TextureSliceEditorMessage::slice( + self.parent_editor, + MessageDirection::ToWidget, + self.texture_slice.clone(), + )); + } +} + impl Deref for TextureSliceEditorWindow { type Target = Widget; @@ -451,13 +481,8 @@ impl Control for TextureSliceEditorWindow { if message.direction() == MessageDirection::FromWidget && message.destination() == self.slice_editor { - // Re-cast the message to parent editor. - ui.send_message( - message - .clone() - .with_destination(self.parent_editor) - .with_direction(MessageDirection::FromWidget), - ); + self.texture_slice = slice.clone(); + self.on_slice_changed(ui); } if message.destination() == self.handle() @@ -472,31 +497,7 @@ impl Control for TextureSliceEditorWindow { self.texture_slice.clone(), )); - ui.send_message(RectEditorMessage::value( - self.region, - MessageDirection::ToWidget, - *self.texture_slice.texture_region, - )); - - for (widget, value) in [ - (self.left_margin, &self.texture_slice.left_margin), - (self.right_margin, &self.texture_slice.right_margin), - (self.top_margin, &self.texture_slice.top_margin), - (self.bottom_margin, &self.texture_slice.bottom_margin), - ] { - ui.send_message(NumericUpDownMessage::value( - widget, - MessageDirection::ToWidget, - **value, - )); - } - - // Send the slice to the parent editor. - ui.send_message(TextureSliceEditorMessage::slice( - self.parent_editor, - MessageDirection::ToWidget, - self.texture_slice.clone(), - )); + self.on_slice_changed(ui); } } else if let Some(NumericUpDownMessage::Value(value)) = message.data::>()