diff --git a/fyrox-ui/src/inspector/editors/mod.rs b/fyrox-ui/src/inspector/editors/mod.rs index 1f460cb05..100d22e07 100644 --- a/fyrox-ui/src/inspector/editors/mod.rs +++ b/fyrox-ui/src/inspector/editors/mod.rs @@ -21,7 +21,7 @@ //! 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::inspector::editors::texture_slice::TextureSlicePropertyEditorDefinition; use crate::{ absm::{EventAction, EventKind}, bit::BitField, @@ -82,7 +82,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 +141,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; @@ -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 new file mode 100644 index 000000000..162673033 --- /dev/null +++ b/fyrox-ui/src/inspector/editors/texture_slice.rs @@ -0,0 +1,800 @@ +// 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::{ + 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}, + inspector::{ + editors::{ + PropertyEditorBuildContext, PropertyEditorDefinition, PropertyEditorInstance, + PropertyEditorMessageContext, PropertyEditorTranslationContext, + }, + FieldKind, InspectorError, PropertyChanged, + }, + message::{CursorIcon, MessageDirection, OsEvent, UiMessage}, + nine_patch::TextureSlice, + numeric::{NumericUpDownBuilder, NumericUpDownMessage}, + rect::{RectEditorBuilder, RectEditorMessage}, + scroll_viewer::ScrollViewerBuilder, + stack_panel::StackPanelBuilder, + text::TextBuilder, + thumb::{ThumbBuilder, ThumbMessage}, + widget::{Widget, WidgetBuilder, WidgetMessage}, + window::{Window, WindowBuilder, WindowMessage, WindowTitle}, + 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 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); + +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_source.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 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()); + + 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; + + 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::(), + }; + + 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(&bounds, 1.0); + drawing_context.commit( + self.clip_bounds(), + self.foreground(), + CommandTexture::Texture(texture.clone()), + None, + ); + + 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, + ); + } + + 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::ToWidget + { + 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(), + )); + } + } + } + } +} + +pub struct TextureSliceEditorBuilder { + widget_builder: WidgetBuilder, + slice: TextureSlice, + 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 { + widget_builder, + slice: Default::default(), + handle_size: 8.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 { + 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 + .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, + })) + } +} + +#[derive(Clone, Reflect, Visit, TypeUuidProvider, ComponentProvider, Debug)] +#[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 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; + + 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 let Some(TextureSliceEditorMessage::Slice(slice)) = message.data() { + if message.direction() == MessageDirection::FromWidget + && message.destination() == self.slice_editor + { + self.texture_slice = slice.clone(); + self.on_slice_changed(ui); + } + + 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(), + )); + + self.on_slice_changed(ui); + } + } else if let Some(NumericUpDownMessage::Value(value)) = + message.data::>() + { + 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, + )); + } + } + } + + 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, + texture_slice: TextureSlice, +} + +impl TextureSliceEditorWindowBuilder { + pub fn new(window_builder: WindowBuilder) -> Self { + 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, 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 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_clip_to_bounds(false) + .with_background(Brush::Solid(Color::WHITE).into()) + .with_foreground(Brush::Solid(Color::GREEN).into()) + .with_margin(Thickness::uniform(3.0)), + ) + .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(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) + } +} + +#[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(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(self.handle, &mut ui.build_ctx()); + + ui.send_message(WindowMessage::open_modal( + self.editor, + MessageDirection::ToWidget, + true, + 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), + ); + } + } + } +} + +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; + +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: TextureSliceFieldEditorBuilder::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> { + let value = ctx.property_info.cast_value::()?; + Ok(Some(TextureSliceEditorMessage::slice( + ctx.instance, + MessageDirection::ToWidget, + value.clone(), + ))) + } + + 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/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/nine_patch.rs b/fyrox-ui/src/nine_patch.rs index d88f6d5fb..9a8324cd8 100644 --- a/fyrox-ui/src/nine_patch.rs +++ b/fyrox-ui/src/nine_patch.rs @@ -60,20 +60,63 @@ 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, - pub texture: InheritableVariable>, +#[derive(Default, Clone, Visit, Reflect, Debug, PartialEq)] +pub struct TextureSlice { + // 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>, +} + +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 { + 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::() @@ -146,11 +189,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 +211,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; @@ -207,18 +250,20 @@ 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_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; @@ -419,7 +464,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, } @@ -433,7 +478,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(), } @@ -465,7 +510,7 @@ impl NinePatchBuilder { } pub fn with_texture_region(mut self, rect: Rect) -> Self { - self.texture_region = Some(rect); + self.texture_region = rect; self } @@ -486,13 +531,16 @@ 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_source: self.texture.clone(), + 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(), + texture: self.texture.into(), stretch_mode: self.stretch_mode.into(), })) } 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)); + } +}