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