use crate::{
border::BorderBuilder,
brush::Brush,
button::{ButtonBuilder, ButtonMessage},
control_trait_proxy_impls,
core::{
algebra::{Matrix3, Vector2},
color::Color,
math::Rect,
pool::Handle,
reflect::prelude::*,
some_or_return,
type_traits::prelude::*,
visitor::prelude::*,
},
decorator::DecoratorBuilder,
define_widget_deref,
draw::{CommandTexture, Draw, DrawingContext},
grid::{Column, GridBuilder, Row},
inspector::{
editors::{
PropertyEditorBuildContext, PropertyEditorDefinition, PropertyEditorInstance,
PropertyEditorMessageContext, PropertyEditorTranslationContext,
},
FieldAction, InspectorError, PropertyChanged,
},
message::{CursorIcon, MessageDirection, 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 crate::button::Button;
use crate::message::MessageData;
use crate::numeric::NumericUpDown;
use crate::rect::RectEditor;
use crate::thumb::Thumb;
use crate::window::WindowAlignment;
use fyrox_texture::TextureKind;
use std::{
any::TypeId,
ops::{Deref, DerefMut},
};
#[derive(Debug, Clone, PartialEq)]
pub enum TextureSliceEditorMessage {
Slice(TextureSlice),
}
impl MessageData for TextureSliceEditorMessage {}
#[derive(Debug, Clone, PartialEq)]
struct DragContext {
initial_position: Vector2<f32>,
bottom_margin: u32,
left_margin: u32,
right_margin: u32,
top_margin: u32,
texture_region: Rect<u32>,
}
#[derive(Clone, Reflect, Visit, TypeUuidProvider, ComponentProvider, Debug)]
#[type_uuid(id = "bd89b59f-13be-4804-bd9c-ed40cfd48b92")]
#[reflect(derived_type = "UiNode")]
pub struct TextureSliceEditor {
widget: Widget,
slice: TextureSlice,
handle_size: f32,
region_min_thumb: Handle<Thumb>,
region_max_thumb: Handle<Thumb>,
slice_min_thumb: Handle<Thumb>,
slice_max_thumb: Handle<Thumb>,
#[reflect(hidden)]
#[visit(skip)]
drag_context: Option<DragContext>,
#[reflect(hidden)]
#[visit(skip)]
scale: f32,
}
impl TextureSliceEditor {
fn sync_thumbs(&self, ui: &UserInterface) {
for (thumb, position) in [
(self.region_min_thumb, self.slice.texture_region.position),
(
self.region_max_thumb,
self.slice.texture_region.right_bottom_corner(),
),
(self.slice_min_thumb, self.slice.margin_min()),
(self.slice_max_thumb, self.slice.margin_max()),
] {
ui.send(
thumb,
WidgetMessage::DesiredPosition(position.cast::<f32>()),
)
}
}
fn on_thumb_dragged(&mut self, thumb: Handle<UiNode>, offset: Vector2<f32>) {
let ctx = some_or_return!(self.drag_context.as_ref());
let texture = some_or_return!(self.slice.texture_source.clone());
let texture_state = texture.state();
let texture_data = some_or_return!(texture_state.data_ref());
let TextureKind::Rectangle { width, height } = texture_data.kind() else {
return;
};
let offset = Vector2::new(offset.x as i32, offset.y as i32);
let margin_min = self.slice.margin_min();
let margin_max = self.slice.margin_max();
let initial_region = ctx.texture_region;
let region = self.slice.texture_region.deref_mut();
if thumb == self.slice_min_thumb {
let top_margin = ctx.top_margin.saturating_add_signed(offset.y);
if top_margin + region.position.y <= margin_max.y {
*self.slice.top_margin = top_margin;
} else {
*self.slice.top_margin = margin_max.y - region.position.y;
}
let left_margin = ctx.left_margin.saturating_add_signed(offset.x);
if left_margin + region.position.x <= margin_max.x {
*self.slice.left_margin = left_margin;
} else {
*self.slice.left_margin = margin_max.x - region.position.x;
}
} else if thumb == self.slice_max_thumb {
let bottom_margin = ctx.bottom_margin.saturating_add_signed(-offset.y);
if (region.position.y + region.size.y).saturating_sub(bottom_margin) >= margin_min.y {
*self.slice.bottom_margin = bottom_margin;
} else {
*self.slice.bottom_margin = region.position.y + region.size.y - margin_min.y;
}
let right_margin = ctx.right_margin.saturating_add_signed(-offset.x);
if (region.position.x + region.size.x).saturating_sub(right_margin) >= margin_min.x {
*self.slice.right_margin = right_margin;
} else {
*self.slice.right_margin = region.position.x + region.size.x - margin_min.x;
}
} else if thumb == self.region_min_thumb {
let x = initial_region.position.x.saturating_add_signed(offset.x);
let max_x = initial_region.position.x + initial_region.size.x;
region.position.x = x.min(max_x);
let y = initial_region.position.y.saturating_add_signed(offset.y);
let max_y = initial_region.position.y + initial_region.size.y;
region.position.y = y.min(max_y);
region.size.x = ctx
.texture_region
.size
.x
.saturating_add_signed(-offset.x)
.min(initial_region.position.x + initial_region.size.x);
region.size.y = ctx
.texture_region
.size
.y
.saturating_add_signed(-offset.y)
.min(initial_region.position.y + initial_region.size.y);
} else if thumb == self.region_max_thumb {
region.size.x = ctx
.texture_region
.size
.x
.saturating_add_signed(offset.x)
.min(width);
region.size.y = ctx
.texture_region
.size
.y
.saturating_add_signed(offset.y)
.min(height);
}
}
}
define_widget_deref!(TextureSliceEditor);
impl Control for TextureSliceEditor {
fn measure_override(&self, ui: &UserInterface, available_size: Vector2<f32>) -> Vector2<f32> {
let mut size: Vector2<f32> = 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<f32>) -> Vector2<f32> {
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());
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()),
&self.material,
None,
);
let mut bounds = Rect {
position: self.slice.texture_region.position.cast::<f32>(),
size: self.slice.texture_region.size.cast::<f32>(),
};
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()),
&self.material,
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;
let thickness = 1.0 / self.scale;
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,
),
thickness,
);
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,
),
thickness,
);
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,
),
thickness,
);
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,
),
thickness,
);
drawing_context.commit(
self.clip_bounds(),
self.foreground(),
CommandTexture::None,
&self.material,
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_for(self.handle()) {
self.slice = slice.clone();
self.sync_thumbs(ui);
} else if let Some(msg) = message.data::<ThumbMessage>() {
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 } => {
self.on_thumb_dragged(message.destination(), *offset);
self.sync_thumbs(ui);
}
ThumbMessage::DragCompleted { .. } => {
self.drag_context = None;
ui.post(
self.handle(),
TextureSliceEditorMessage::Slice(self.slice.clone()),
);
}
}
} else if let Some(WidgetMessage::MouseWheel { amount, .. }) = message.data() {
self.scale = (self.scale + 0.1 * *amount).clamp(1.0, 10.0);
ui.send(
self.handle,
WidgetMessage::LayoutTransform(Matrix3::new_scaling(self.scale)),
);
for thumb in [
self.slice_min_thumb,
self.slice_max_thumb,
self.region_min_thumb,
self.region_max_thumb,
] {
ui.send(thumb, WidgetMessage::Width(self.handle_size / self.scale));
ui.send(thumb, WidgetMessage::Height(self.handle_size / self.scale));
}
}
}
}
pub struct TextureSliceEditorBuilder {
widget_builder: WidgetBuilder,
slice: TextureSlice,
handle_size: f32,
}
fn make_thumb(position: Vector2<u32>, handle_size: f32, ctx: &mut BuildContext) -> Handle<Thumb> {
ThumbBuilder::new(
WidgetBuilder::new()
.with_desired_position(position.cast::<f32>())
.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<TextureSliceEditor> {
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(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,
scale: 1.0,
})
}
}
#[derive(Clone, Reflect, Visit, TypeUuidProvider, ComponentProvider, Debug)]
#[type_uuid(id = "0293081d-55fd-4aa2-a06e-d53fba1a2617")]
#[reflect(derived_type = "UiNode")]
pub struct TextureSliceEditorWindow {
window: Window,
parent_editor: Handle<UiNode>,
slice_editor: Handle<TextureSliceEditor>,
texture_slice: TextureSlice,
left_margin: Handle<NumericUpDown<u32>>,
right_margin: Handle<NumericUpDown<u32>>,
top_margin: Handle<NumericUpDown<u32>>,
bottom_margin: Handle<NumericUpDown<u32>>,
region: Handle<RectEditor<u32>>,
}
impl TextureSliceEditorWindow {
fn on_slice_changed(&self, ui: &UserInterface) {
ui.send(
self.region,
RectEditorMessage::Value(*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(widget, NumericUpDownMessage::Value(**value));
}
ui.send(
self.parent_editor,
TextureSliceEditorMessage::Slice(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 {
control_trait_proxy_impls!(window);
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.is_from(self.slice_editor) {
self.texture_slice = slice.clone();
self.on_slice_changed(ui);
}
if message.is_for(self.handle()) && &self.texture_slice != slice {
self.texture_slice = slice.clone();
ui.send(
self.slice_editor,
TextureSliceEditorMessage::Slice(self.texture_slice.clone()),
);
self.on_slice_changed(ui);
}
} else if let Some(NumericUpDownMessage::Value(value)) =
message.data::<NumericUpDownMessage<u32>>()
{
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(self.handle, TextureSliceEditorMessage::Slice(slice));
}
}
} else if let Some(RectEditorMessage::Value(value)) =
message.data_from::<RectEditorMessage<u32>>(self.region)
{
let mut slice = self.texture_slice.clone();
slice.texture_region.set_value_and_mark_modified(*value);
ui.send(self.handle, TextureSliceEditorMessage::Slice(slice));
}
}
}
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<UiNode>,
ctx: &mut BuildContext,
) -> Handle<TextureSliceEditorWindow> {
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_horizontal_scroll_allowed(true)
.with_vertical_scroll_allowed(true)
.with_content(slice_editor)
.with_h_scroll_speed(0.0)
.with_v_scroll_speed(0.0)
.build(ctx);
let content = GridBuilder::new(
WidgetBuilder::new()
.with_child(toolbar)
.with_child(scroll_viewer),
)
.add_column(Column::strict(200.0))
.add_column(Column::stretch())
.add_row(Row::stretch())
.build(ctx);
let node = 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)
}
}
#[derive(Clone, Reflect, Visit, TypeUuidProvider, ComponentProvider, Debug)]
#[type_uuid(id = "024f3a3a-6784-4675-bd99-a4c6c19a8d91")]
#[reflect(derived_type = "UiNode")]
pub struct TextureSliceFieldEditor {
widget: Widget,
texture_slice: TextureSlice,
edit: Handle<Button>,
editor: Handle<TextureSliceEditorWindow>,
}
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(
self.editor,
WindowMessage::Open {
alignment: WindowAlignment::Center,
modal: true,
focus_content: true,
},
);
}
} else if let Some(TextureSliceEditorMessage::Slice(slice)) = message.data_for(self.handle)
{
if &self.texture_slice != slice {
self.texture_slice = slice.clone();
ui.try_send_response(message);
ui.send(
self.editor,
TextureSliceEditorMessage::Slice(self.texture_slice.clone()),
);
}
}
}
}
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<TextureSliceFieldEditor> {
let edit = ButtonBuilder::new(WidgetBuilder::new())
.with_text("Edit...")
.build(ctx);
let node = TextureSliceFieldEditor {
widget: self.widget_builder.with_child(edit).build(ctx),
texture_slice: self.texture_slice,
edit,
editor: Default::default(),
};
ctx.add(node)
}
}
#[derive(Debug)]
pub struct TextureSlicePropertyEditorDefinition;
impl PropertyEditorDefinition for TextureSlicePropertyEditorDefinition {
fn value_type_id(&self) -> TypeId {
TypeId::of::<TextureSlice>()
}
fn create_instance(
&self,
ctx: PropertyEditorBuildContext,
) -> Result<PropertyEditorInstance, InspectorError> {
let value = ctx.property_info.cast_value::<TextureSlice>()?;
Ok(PropertyEditorInstance::simple(
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<Option<UiMessage>, InspectorError> {
let value = ctx.property_info.cast_value::<TextureSlice>()?;
Ok(Some(
UiMessage::with_data(TextureSliceEditorMessage::Slice(value.clone()))
.with_destination(ctx.instance),
))
}
fn translate_message(&self, ctx: PropertyEditorTranslationContext) -> Option<PropertyChanged> {
if ctx.message.direction() == MessageDirection::FromWidget {
if let Some(TextureSliceEditorMessage::Slice(value)) = ctx.message.data() {
return Some(PropertyChanged {
name: ctx.name.to_string(),
action: FieldAction::object(value.clone()),
});
}
}
None
}
}