fyrox_ui/inspector/editors/
texture_slice.rs

1// Copyright (c) 2019-present Dmitry Stepanov and Fyrox Engine contributors.
2//
3// Permission is hereby granted, free of charge, to any person obtaining a copy
4// of this software and associated documentation files (the "Software"), to deal
5// in the Software without restriction, including without limitation the rights
6// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
7// copies of the Software, and to permit persons to whom the Software is
8// furnished to do so, subject to the following conditions:
9//
10// The above copyright notice and this permission notice shall be included in all
11// copies or substantial portions of the Software.
12//
13// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
14// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
15// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
16// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
17// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
18// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
19// SOFTWARE.
20
21use crate::{
22    border::BorderBuilder,
23    brush::Brush,
24    button::{ButtonBuilder, ButtonMessage},
25    core::{
26        algebra::{Matrix3, Vector2},
27        color::Color,
28        math::Rect,
29        pool::Handle,
30        reflect::prelude::*,
31        some_or_return,
32        type_traits::prelude::*,
33        visitor::prelude::*,
34    },
35    decorator::DecoratorBuilder,
36    define_constructor, define_widget_deref,
37    draw::{CommandTexture, Draw, DrawingContext},
38    grid::{Column, GridBuilder, Row},
39    inspector::{
40        editors::{
41            PropertyEditorBuildContext, PropertyEditorDefinition, PropertyEditorInstance,
42            PropertyEditorMessageContext, PropertyEditorTranslationContext,
43        },
44        FieldKind, InspectorError, PropertyChanged,
45    },
46    message::{CursorIcon, MessageDirection, OsEvent, UiMessage},
47    nine_patch::TextureSlice,
48    numeric::{NumericUpDownBuilder, NumericUpDownMessage},
49    rect::{RectEditorBuilder, RectEditorMessage},
50    scroll_viewer::ScrollViewerBuilder,
51    stack_panel::StackPanelBuilder,
52    text::TextBuilder,
53    thumb::{ThumbBuilder, ThumbMessage},
54    widget::{Widget, WidgetBuilder, WidgetMessage},
55    window::{Window, WindowBuilder, WindowMessage, WindowTitle},
56    BuildContext, Control, Thickness, UiNode, UserInterface, VerticalAlignment,
57};
58use fyrox_texture::TextureKind;
59use std::{
60    any::TypeId,
61    ops::{Deref, DerefMut},
62    sync::mpsc::Sender,
63};
64
65#[derive(Debug, Clone, PartialEq)]
66pub enum TextureSliceEditorMessage {
67    Slice(TextureSlice),
68}
69
70impl TextureSliceEditorMessage {
71    define_constructor!(TextureSliceEditorMessage:Slice => fn slice(TextureSlice), layout: false);
72}
73
74#[derive(Debug, Clone, PartialEq)]
75struct DragContext {
76    initial_position: Vector2<f32>,
77    bottom_margin: u32,
78    left_margin: u32,
79    right_margin: u32,
80    top_margin: u32,
81    texture_region: Rect<u32>,
82}
83
84#[derive(Clone, Reflect, Visit, TypeUuidProvider, ComponentProvider, Debug)]
85#[type_uuid(id = "bd89b59f-13be-4804-bd9c-ed40cfd48b92")]
86pub struct TextureSliceEditor {
87    widget: Widget,
88    slice: TextureSlice,
89    handle_size: f32,
90    region_min_thumb: Handle<UiNode>,
91    region_max_thumb: Handle<UiNode>,
92    slice_min_thumb: Handle<UiNode>,
93    slice_max_thumb: Handle<UiNode>,
94    #[reflect(hidden)]
95    #[visit(skip)]
96    drag_context: Option<DragContext>,
97    #[reflect(hidden)]
98    #[visit(skip)]
99    scale: f32,
100}
101
102impl TextureSliceEditor {
103    fn sync_thumbs(&self, ui: &UserInterface) {
104        for (thumb, position) in [
105            (self.region_min_thumb, self.slice.texture_region.position),
106            (
107                self.region_max_thumb,
108                self.slice.texture_region.right_bottom_corner(),
109            ),
110            (self.slice_min_thumb, self.slice.margin_min()),
111            (self.slice_max_thumb, self.slice.margin_max()),
112        ] {
113            ui.send_message(WidgetMessage::desired_position(
114                thumb,
115                MessageDirection::ToWidget,
116                position.cast::<f32>(),
117            ))
118        }
119    }
120
121    fn on_thumb_dragged(&mut self, thumb: Handle<UiNode>, offset: Vector2<f32>) {
122        let ctx = some_or_return!(self.drag_context.as_ref());
123        let texture = some_or_return!(self.slice.texture_source.clone());
124        let texture_state = texture.state();
125        let texture_data = some_or_return!(texture_state.data_ref());
126        let TextureKind::Rectangle { width, height } = texture_data.kind() else {
127            return;
128        };
129
130        let offset = Vector2::new(offset.x as i32, offset.y as i32);
131
132        let margin_min = self.slice.margin_min();
133        let margin_max = self.slice.margin_max();
134        let initial_region = ctx.texture_region;
135        let region = self.slice.texture_region.deref_mut();
136
137        if thumb == self.slice_min_thumb {
138            let top_margin = ctx.top_margin.saturating_add_signed(offset.y);
139            if top_margin + region.position.y <= margin_max.y {
140                *self.slice.top_margin = top_margin;
141            } else {
142                *self.slice.top_margin = margin_max.y - region.position.y;
143            }
144
145            let left_margin = ctx.left_margin.saturating_add_signed(offset.x);
146            if left_margin + region.position.x <= margin_max.x {
147                *self.slice.left_margin = left_margin;
148            } else {
149                *self.slice.left_margin = margin_max.x - region.position.x;
150            }
151        } else if thumb == self.slice_max_thumb {
152            let bottom_margin = ctx.bottom_margin.saturating_add_signed(-offset.y);
153            if (region.position.y + region.size.y).saturating_sub(bottom_margin) >= margin_min.y {
154                *self.slice.bottom_margin = bottom_margin;
155            } else {
156                *self.slice.bottom_margin = region.position.y + region.size.y - margin_min.y;
157            }
158
159            let right_margin = ctx.right_margin.saturating_add_signed(-offset.x);
160            if (region.position.x + region.size.x).saturating_sub(right_margin) >= margin_min.x {
161                *self.slice.right_margin = right_margin;
162            } else {
163                *self.slice.right_margin = region.position.x + region.size.x - margin_min.x;
164            }
165        } else if thumb == self.region_min_thumb {
166            let x = initial_region.position.x.saturating_add_signed(offset.x);
167            let max_x = initial_region.position.x + initial_region.size.x;
168            region.position.x = x.min(max_x);
169
170            let y = initial_region.position.y.saturating_add_signed(offset.y);
171            let max_y = initial_region.position.y + initial_region.size.y;
172            region.position.y = y.min(max_y);
173
174            region.size.x = ctx
175                .texture_region
176                .size
177                .x
178                .saturating_add_signed(-offset.x)
179                .min(initial_region.position.x + initial_region.size.x);
180            region.size.y = ctx
181                .texture_region
182                .size
183                .y
184                .saturating_add_signed(-offset.y)
185                .min(initial_region.position.y + initial_region.size.y);
186        } else if thumb == self.region_max_thumb {
187            region.size.x = ctx
188                .texture_region
189                .size
190                .x
191                .saturating_add_signed(offset.x)
192                .min(width);
193            region.size.y = ctx
194                .texture_region
195                .size
196                .y
197                .saturating_add_signed(offset.y)
198                .min(height);
199        }
200    }
201}
202
203define_widget_deref!(TextureSliceEditor);
204
205impl Control for TextureSliceEditor {
206    fn measure_override(&self, ui: &UserInterface, available_size: Vector2<f32>) -> Vector2<f32> {
207        let mut size: Vector2<f32> = self.widget.measure_override(ui, available_size);
208
209        if let Some(texture) = self.slice.texture_source.as_ref() {
210            let state = texture.state();
211            if let Some(data) = state.data_ref() {
212                if let TextureKind::Rectangle { width, height } = data.kind() {
213                    let width = width as f32;
214                    let height = height as f32;
215                    if size.x < width {
216                        size.x = width;
217                    }
218                    if size.y < height {
219                        size.y = height;
220                    }
221                }
222            }
223        }
224
225        size
226    }
227
228    fn arrange_override(&self, ui: &UserInterface, final_size: Vector2<f32>) -> Vector2<f32> {
229        for &child_handle in self.widget.children() {
230            let child = ui.nodes.borrow(child_handle);
231            ui.arrange_node(
232                child_handle,
233                &Rect::new(
234                    child.desired_local_position().x,
235                    child.desired_local_position().y,
236                    child.desired_size().x,
237                    child.desired_size().y,
238                ),
239            );
240        }
241
242        final_size
243    }
244
245    fn draw(&self, drawing_context: &mut DrawingContext) {
246        let texture = some_or_return!(self.slice.texture_source.clone());
247
248        let state = texture.state();
249        let texture_data = some_or_return!(state.data_ref());
250
251        // Only 2D textures can be used with nine-patch.
252        let TextureKind::Rectangle { width, height } = texture_data.kind() else {
253            return;
254        };
255
256        let texture_width = width as f32;
257        let texture_height = height as f32;
258
259        drawing_context.push_rect_filled(&Rect::new(0.0, 0.0, texture_width, texture_height), None);
260        drawing_context.commit(
261            self.clip_bounds(),
262            self.background(),
263            CommandTexture::Texture(texture.clone()),
264            None,
265        );
266
267        let mut bounds = Rect {
268            position: self.slice.texture_region.position.cast::<f32>(),
269            size: self.slice.texture_region.size.cast::<f32>(),
270        };
271
272        if bounds.size.x == 0.0 && bounds.size.y == 0.0 {
273            bounds.size.x = texture_width;
274            bounds.size.y = texture_height;
275        }
276
277        drawing_context.push_rect(&bounds, 1.0);
278        drawing_context.commit(
279            self.clip_bounds(),
280            self.foreground(),
281            CommandTexture::Texture(texture.clone()),
282            None,
283        );
284
285        let left_margin = *self.slice.left_margin as f32;
286        let right_margin = *self.slice.right_margin as f32;
287        let top_margin = *self.slice.top_margin as f32;
288        let bottom_margin = *self.slice.bottom_margin as f32;
289        let thickness = 1.0 / self.scale;
290
291        // Draw nine slices.
292        drawing_context.push_line(
293            Vector2::new(bounds.position.x + left_margin, bounds.position.y),
294            Vector2::new(
295                bounds.position.x + left_margin,
296                bounds.position.y + bounds.size.y,
297            ),
298            thickness,
299        );
300        drawing_context.push_line(
301            Vector2::new(
302                bounds.position.x + bounds.size.x - right_margin,
303                bounds.position.y,
304            ),
305            Vector2::new(
306                bounds.position.x + bounds.size.x - right_margin,
307                bounds.position.y + bounds.size.y,
308            ),
309            thickness,
310        );
311        drawing_context.push_line(
312            Vector2::new(bounds.position.x, bounds.position.y + top_margin),
313            Vector2::new(
314                bounds.position.x + bounds.size.x,
315                bounds.position.y + top_margin,
316            ),
317            thickness,
318        );
319        drawing_context.push_line(
320            Vector2::new(
321                bounds.position.x,
322                bounds.position.y + bounds.size.y - bottom_margin,
323            ),
324            Vector2::new(
325                bounds.position.x + bounds.size.x,
326                bounds.position.y + bounds.size.y - bottom_margin,
327            ),
328            thickness,
329        );
330        drawing_context.commit(
331            self.clip_bounds(),
332            self.foreground(),
333            CommandTexture::None,
334            None,
335        );
336    }
337
338    fn handle_routed_message(&mut self, ui: &mut UserInterface, message: &mut UiMessage) {
339        self.widget.handle_routed_message(ui, message);
340
341        if let Some(TextureSliceEditorMessage::Slice(slice)) = message.data() {
342            if message.destination() == self.handle()
343                && message.direction() == MessageDirection::ToWidget
344            {
345                self.slice = slice.clone();
346                self.sync_thumbs(ui);
347            }
348        } else if let Some(msg) = message.data::<ThumbMessage>() {
349            match msg {
350                ThumbMessage::DragStarted { position } => {
351                    self.drag_context = Some(DragContext {
352                        initial_position: *position,
353                        bottom_margin: *self.slice.bottom_margin,
354                        left_margin: *self.slice.left_margin,
355                        right_margin: *self.slice.right_margin,
356                        top_margin: *self.slice.top_margin,
357                        texture_region: *self.slice.texture_region,
358                    });
359                }
360                ThumbMessage::DragDelta { offset } => {
361                    self.on_thumb_dragged(message.destination(), *offset);
362                    self.sync_thumbs(ui);
363                }
364                ThumbMessage::DragCompleted { .. } => {
365                    self.drag_context = None;
366                    ui.send_message(TextureSliceEditorMessage::slice(
367                        self.handle(),
368                        MessageDirection::FromWidget,
369                        self.slice.clone(),
370                    ));
371                }
372            }
373        } else if let Some(WidgetMessage::MouseWheel { amount, .. }) = message.data() {
374            self.scale = (self.scale + 0.1 * *amount).clamp(1.0, 10.0);
375
376            ui.send_message(WidgetMessage::layout_transform(
377                self.handle,
378                MessageDirection::ToWidget,
379                Matrix3::new_scaling(self.scale),
380            ));
381
382            for thumb in [
383                self.slice_min_thumb,
384                self.slice_max_thumb,
385                self.region_min_thumb,
386                self.region_max_thumb,
387            ] {
388                ui.send_message(WidgetMessage::width(
389                    thumb,
390                    MessageDirection::ToWidget,
391                    self.handle_size / self.scale,
392                ));
393                ui.send_message(WidgetMessage::height(
394                    thumb,
395                    MessageDirection::ToWidget,
396                    self.handle_size / self.scale,
397                ));
398            }
399        }
400    }
401}
402
403pub struct TextureSliceEditorBuilder {
404    widget_builder: WidgetBuilder,
405    slice: TextureSlice,
406    handle_size: f32,
407}
408
409fn make_thumb(position: Vector2<u32>, handle_size: f32, ctx: &mut BuildContext) -> Handle<UiNode> {
410    ThumbBuilder::new(
411        WidgetBuilder::new()
412            .with_desired_position(position.cast::<f32>())
413            .with_child(
414                DecoratorBuilder::new(BorderBuilder::new(
415                    WidgetBuilder::new()
416                        .with_width(handle_size)
417                        .with_height(handle_size)
418                        .with_cursor(Some(CursorIcon::Grab))
419                        .with_foreground(Brush::Solid(Color::opaque(0, 150, 0)).into()),
420                ))
421                .with_pressable(false)
422                .with_selected(false)
423                .with_normal_brush(Brush::Solid(Color::opaque(0, 150, 0)).into())
424                .with_hover_brush(Brush::Solid(Color::opaque(0, 255, 0)).into())
425                .build(ctx),
426            ),
427    )
428    .build(ctx)
429}
430
431impl TextureSliceEditorBuilder {
432    pub fn new(widget_builder: WidgetBuilder) -> Self {
433        Self {
434            widget_builder,
435            slice: Default::default(),
436            handle_size: 8.0,
437        }
438    }
439
440    pub fn with_texture_slice(mut self, slice: TextureSlice) -> Self {
441        self.slice = slice;
442        self
443    }
444
445    pub fn with_handle_size(mut self, size: f32) -> Self {
446        self.handle_size = size;
447        self
448    }
449
450    pub fn build(self, ctx: &mut BuildContext) -> Handle<UiNode> {
451        let region_min_thumb =
452            make_thumb(self.slice.texture_region.position, self.handle_size, ctx);
453        let region_max_thumb = make_thumb(
454            self.slice.texture_region.right_bottom_corner(),
455            self.handle_size,
456            ctx,
457        );
458        let slice_min_thumb = make_thumb(self.slice.margin_min(), self.handle_size, ctx);
459        let slice_max_thumb = make_thumb(self.slice.margin_max(), self.handle_size, ctx);
460
461        ctx.add_node(UiNode::new(TextureSliceEditor {
462            widget: self
463                .widget_builder
464                .with_child(region_min_thumb)
465                .with_child(region_max_thumb)
466                .with_child(slice_min_thumb)
467                .with_child(slice_max_thumb)
468                .build(ctx),
469            slice: self.slice,
470            handle_size: self.handle_size,
471            region_min_thumb,
472            region_max_thumb,
473            slice_min_thumb,
474            slice_max_thumb,
475            drag_context: None,
476            scale: 1.0,
477        }))
478    }
479}
480
481#[derive(Clone, Reflect, Visit, TypeUuidProvider, ComponentProvider, Debug)]
482#[type_uuid(id = "0293081d-55fd-4aa2-a06e-d53fba1a2617")]
483pub struct TextureSliceEditorWindow {
484    window: Window,
485    parent_editor: Handle<UiNode>,
486    slice_editor: Handle<UiNode>,
487    texture_slice: TextureSlice,
488    left_margin: Handle<UiNode>,
489    right_margin: Handle<UiNode>,
490    top_margin: Handle<UiNode>,
491    bottom_margin: Handle<UiNode>,
492    region: Handle<UiNode>,
493}
494
495impl TextureSliceEditorWindow {
496    fn on_slice_changed(&self, ui: &UserInterface) {
497        ui.send_message(RectEditorMessage::value(
498            self.region,
499            MessageDirection::ToWidget,
500            *self.texture_slice.texture_region,
501        ));
502
503        for (widget, value) in [
504            (self.left_margin, &self.texture_slice.left_margin),
505            (self.right_margin, &self.texture_slice.right_margin),
506            (self.top_margin, &self.texture_slice.top_margin),
507            (self.bottom_margin, &self.texture_slice.bottom_margin),
508        ] {
509            ui.send_message(NumericUpDownMessage::value(
510                widget,
511                MessageDirection::ToWidget,
512                **value,
513            ));
514        }
515
516        // Send the slice to the parent editor.
517        ui.send_message(TextureSliceEditorMessage::slice(
518            self.parent_editor,
519            MessageDirection::ToWidget,
520            self.texture_slice.clone(),
521        ));
522    }
523}
524
525impl Deref for TextureSliceEditorWindow {
526    type Target = Widget;
527
528    fn deref(&self) -> &Self::Target {
529        &self.window.widget
530    }
531}
532
533impl DerefMut for TextureSliceEditorWindow {
534    fn deref_mut(&mut self) -> &mut Self::Target {
535        &mut self.window.widget
536    }
537}
538
539impl Control for TextureSliceEditorWindow {
540    fn on_remove(&self, sender: &Sender<UiMessage>) {
541        self.window.on_remove(sender)
542    }
543
544    fn measure_override(&self, ui: &UserInterface, available_size: Vector2<f32>) -> Vector2<f32> {
545        self.window.measure_override(ui, available_size)
546    }
547
548    fn arrange_override(&self, ui: &UserInterface, final_size: Vector2<f32>) -> Vector2<f32> {
549        self.window.arrange_override(ui, final_size)
550    }
551
552    fn draw(&self, drawing_context: &mut DrawingContext) {
553        self.window.draw(drawing_context)
554    }
555
556    fn on_visual_transform_changed(&self) {
557        self.window.on_visual_transform_changed()
558    }
559
560    fn post_draw(&self, drawing_context: &mut DrawingContext) {
561        self.window.post_draw(drawing_context)
562    }
563
564    fn update(&mut self, dt: f32, ui: &mut UserInterface) {
565        self.window.update(dt, ui);
566    }
567
568    fn handle_routed_message(&mut self, ui: &mut UserInterface, message: &mut UiMessage) {
569        self.window.handle_routed_message(ui, message);
570        if let Some(TextureSliceEditorMessage::Slice(slice)) = message.data() {
571            if message.direction() == MessageDirection::FromWidget
572                && message.destination() == self.slice_editor
573            {
574                self.texture_slice = slice.clone();
575                self.on_slice_changed(ui);
576            }
577
578            if message.destination() == self.handle()
579                && message.direction() == MessageDirection::ToWidget
580                && &self.texture_slice != slice
581            {
582                self.texture_slice = slice.clone();
583
584                ui.send_message(TextureSliceEditorMessage::slice(
585                    self.slice_editor,
586                    MessageDirection::ToWidget,
587                    self.texture_slice.clone(),
588                ));
589
590                self.on_slice_changed(ui);
591            }
592        } else if let Some(NumericUpDownMessage::Value(value)) =
593            message.data::<NumericUpDownMessage<u32>>()
594        {
595            if message.direction() == MessageDirection::FromWidget {
596                let mut slice = self.texture_slice.clone();
597                let mut target = None;
598                for (widget, margin) in [
599                    (self.left_margin, &mut slice.left_margin),
600                    (self.right_margin, &mut slice.right_margin),
601                    (self.top_margin, &mut slice.top_margin),
602                    (self.bottom_margin, &mut slice.bottom_margin),
603                ] {
604                    if message.destination() == widget {
605                        margin.set_value_and_mark_modified(*value);
606                        target = Some(widget);
607                        break;
608                    }
609                }
610                if target.is_some() {
611                    ui.send_message(TextureSliceEditorMessage::slice(
612                        self.handle,
613                        MessageDirection::ToWidget,
614                        slice,
615                    ));
616                }
617            }
618        } else if let Some(RectEditorMessage::Value(value)) =
619            message.data::<RectEditorMessage<u32>>()
620        {
621            if message.direction() == MessageDirection::FromWidget
622                && message.destination() == self.region
623            {
624                let mut slice = self.texture_slice.clone();
625                slice.texture_region.set_value_and_mark_modified(*value);
626                ui.send_message(TextureSliceEditorMessage::slice(
627                    self.handle,
628                    MessageDirection::ToWidget,
629                    slice,
630                ));
631            }
632        }
633    }
634
635    fn preview_message(&self, ui: &UserInterface, message: &mut UiMessage) {
636        self.window.preview_message(ui, message);
637    }
638
639    fn handle_os_event(
640        &mut self,
641        self_handle: Handle<UiNode>,
642        ui: &mut UserInterface,
643        event: &OsEvent,
644    ) {
645        self.window.handle_os_event(self_handle, ui, event);
646    }
647}
648
649pub struct TextureSliceEditorWindowBuilder {
650    window_builder: WindowBuilder,
651    texture_slice: TextureSlice,
652}
653
654impl TextureSliceEditorWindowBuilder {
655    pub fn new(window_builder: WindowBuilder) -> Self {
656        Self {
657            window_builder,
658            texture_slice: Default::default(),
659        }
660    }
661
662    pub fn with_texture_slice(mut self, slice: TextureSlice) -> Self {
663        self.texture_slice = slice;
664        self
665    }
666
667    pub fn build(self, parent_editor: Handle<UiNode>, ctx: &mut BuildContext) -> Handle<UiNode> {
668        let region_text = TextBuilder::new(WidgetBuilder::new())
669            .with_text("Texture Region")
670            .build(ctx);
671        let region =
672            RectEditorBuilder::new(WidgetBuilder::new().with_margin(Thickness::uniform(1.0)))
673                .with_value(*self.texture_slice.texture_region)
674                .build(ctx);
675        let left_margin_text = TextBuilder::new(WidgetBuilder::new())
676            .with_text("Left Margin")
677            .build(ctx);
678        let left_margin =
679            NumericUpDownBuilder::new(WidgetBuilder::new().with_margin(Thickness::uniform(1.0)))
680                .with_value(*self.texture_slice.left_margin)
681                .build(ctx);
682        let right_margin_text = TextBuilder::new(WidgetBuilder::new())
683            .with_text("Right Margin")
684            .build(ctx);
685        let right_margin =
686            NumericUpDownBuilder::new(WidgetBuilder::new().with_margin(Thickness::uniform(1.0)))
687                .with_value(*self.texture_slice.right_margin)
688                .build(ctx);
689        let top_margin_text = TextBuilder::new(WidgetBuilder::new())
690            .with_text("Top Margin")
691            .build(ctx);
692        let top_margin =
693            NumericUpDownBuilder::new(WidgetBuilder::new().with_margin(Thickness::uniform(1.0)))
694                .with_value(*self.texture_slice.top_margin)
695                .build(ctx);
696        let bottom_margin_text = TextBuilder::new(WidgetBuilder::new())
697            .with_text("Bottom Margin")
698            .build(ctx);
699        let bottom_margin =
700            NumericUpDownBuilder::new(WidgetBuilder::new().with_margin(Thickness::uniform(1.0)))
701                .with_value(*self.texture_slice.bottom_margin)
702                .build(ctx);
703
704        let toolbar = StackPanelBuilder::new(
705            WidgetBuilder::new()
706                .with_child(region_text)
707                .with_child(region)
708                .with_child(left_margin_text)
709                .with_child(left_margin)
710                .with_child(right_margin_text)
711                .with_child(right_margin)
712                .with_child(top_margin_text)
713                .with_child(top_margin)
714                .with_child(bottom_margin_text)
715                .with_child(bottom_margin)
716                .on_column(0),
717        )
718        .build(ctx);
719
720        let slice_editor = TextureSliceEditorBuilder::new(
721            WidgetBuilder::new()
722                .with_clip_to_bounds(false)
723                .with_background(Brush::Solid(Color::WHITE).into())
724                .with_foreground(Brush::Solid(Color::GREEN).into())
725                .with_margin(Thickness::uniform(3.0)),
726        )
727        .with_texture_slice(self.texture_slice.clone())
728        .build(ctx);
729        let scroll_viewer = ScrollViewerBuilder::new(WidgetBuilder::new().on_column(1))
730            .with_horizontal_scroll_allowed(true)
731            .with_vertical_scroll_allowed(true)
732            .with_content(slice_editor)
733            // Disable scrolling via mouse wheel. Mouse wheel is used to change zoom.
734            .with_h_scroll_speed(0.0)
735            .with_v_scroll_speed(0.0)
736            .build(ctx);
737        let content = GridBuilder::new(
738            WidgetBuilder::new()
739                .with_child(toolbar)
740                .with_child(scroll_viewer),
741        )
742        .add_column(Column::strict(200.0))
743        .add_column(Column::stretch())
744        .add_row(Row::stretch())
745        .build(ctx);
746
747        let node = UiNode::new(TextureSliceEditorWindow {
748            window: self.window_builder.with_content(content).build_window(ctx),
749            parent_editor,
750            slice_editor,
751            texture_slice: self.texture_slice,
752            left_margin,
753            right_margin,
754            top_margin,
755            bottom_margin,
756            region,
757        });
758
759        ctx.add_node(node)
760    }
761}
762
763#[derive(Clone, Reflect, Visit, TypeUuidProvider, ComponentProvider, Debug)]
764#[type_uuid(id = "024f3a3a-6784-4675-bd99-a4c6c19a8d91")]
765pub struct TextureSliceFieldEditor {
766    widget: Widget,
767    texture_slice: TextureSlice,
768    edit: Handle<UiNode>,
769    editor: Handle<UiNode>,
770}
771
772define_widget_deref!(TextureSliceFieldEditor);
773
774impl Control for TextureSliceFieldEditor {
775    fn handle_routed_message(&mut self, ui: &mut UserInterface, message: &mut UiMessage) {
776        self.widget.handle_routed_message(ui, message);
777
778        if let Some(ButtonMessage::Click) = message.data() {
779            if message.destination() == self.edit {
780                self.editor = TextureSliceEditorWindowBuilder::new(
781                    WindowBuilder::new(WidgetBuilder::new().with_width(700.0).with_height(500.0))
782                        .with_title(WindowTitle::text("Texture Slice Editor"))
783                        .open(false)
784                        .with_remove_on_close(true),
785                )
786                .with_texture_slice(self.texture_slice.clone())
787                .build(self.handle, &mut ui.build_ctx());
788
789                ui.send_message(WindowMessage::open_modal(
790                    self.editor,
791                    MessageDirection::ToWidget,
792                    true,
793                    true,
794                ));
795            }
796        } else if let Some(TextureSliceEditorMessage::Slice(slice)) = message.data() {
797            if message.destination() == self.handle
798                && message.direction() == MessageDirection::ToWidget
799                && &self.texture_slice != slice
800            {
801                self.texture_slice = slice.clone();
802                ui.send_message(message.reverse());
803                ui.send_message(TextureSliceEditorMessage::slice(
804                    self.editor,
805                    MessageDirection::ToWidget,
806                    self.texture_slice.clone(),
807                ));
808            }
809        }
810    }
811}
812
813pub struct TextureSliceFieldEditorBuilder {
814    widget_builder: WidgetBuilder,
815    texture_slice: TextureSlice,
816}
817
818impl TextureSliceFieldEditorBuilder {
819    pub fn new(widget_builder: WidgetBuilder) -> Self {
820        Self {
821            widget_builder,
822            texture_slice: Default::default(),
823        }
824    }
825
826    pub fn with_texture_slice(mut self, slice: TextureSlice) -> Self {
827        self.texture_slice = slice;
828        self
829    }
830
831    pub fn build(self, ctx: &mut BuildContext) -> Handle<UiNode> {
832        let edit = ButtonBuilder::new(WidgetBuilder::new())
833            .with_text("Edit...")
834            .build(ctx);
835
836        let node = UiNode::new(TextureSliceFieldEditor {
837            widget: self.widget_builder.with_child(edit).build(ctx),
838            texture_slice: self.texture_slice,
839            edit,
840            editor: Default::default(),
841        });
842        ctx.add_node(node)
843    }
844}
845
846#[derive(Debug)]
847pub struct TextureSlicePropertyEditorDefinition;
848
849impl PropertyEditorDefinition for TextureSlicePropertyEditorDefinition {
850    fn value_type_id(&self) -> TypeId {
851        TypeId::of::<TextureSlice>()
852    }
853
854    fn create_instance(
855        &self,
856        ctx: PropertyEditorBuildContext,
857    ) -> Result<PropertyEditorInstance, InspectorError> {
858        let value = ctx.property_info.cast_value::<TextureSlice>()?;
859        Ok(PropertyEditorInstance::Simple {
860            editor: TextureSliceFieldEditorBuilder::new(
861                WidgetBuilder::new()
862                    .with_margin(Thickness::top_bottom(1.0))
863                    .with_vertical_alignment(VerticalAlignment::Center),
864            )
865            .with_texture_slice(value.clone())
866            .build(ctx.build_context),
867        })
868    }
869
870    fn create_message(
871        &self,
872        ctx: PropertyEditorMessageContext,
873    ) -> Result<Option<UiMessage>, InspectorError> {
874        let value = ctx.property_info.cast_value::<TextureSlice>()?;
875        Ok(Some(TextureSliceEditorMessage::slice(
876            ctx.instance,
877            MessageDirection::ToWidget,
878            value.clone(),
879        )))
880    }
881
882    fn translate_message(&self, ctx: PropertyEditorTranslationContext) -> Option<PropertyChanged> {
883        if ctx.message.direction() == MessageDirection::FromWidget {
884            if let Some(TextureSliceEditorMessage::Slice(value)) = ctx.message.data() {
885                return Some(PropertyChanged {
886                    name: ctx.name.to_string(),
887                    owner_type_id: ctx.owner_type_id,
888                    value: FieldKind::object(value.clone()),
889                });
890            }
891        }
892        None
893    }
894}