From 78848617c0944cac83a386b90cadec74eede2f6a Mon Sep 17 00:00:00 2001 From: Dmitry Stepanov Date: Mon, 27 Jan 2025 20:31:16 +0300 Subject: [PATCH] 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(), }))