Skip to main content

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