Skip to main content

fyrox_ui/
nine_patch.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    brush::Brush,
23    core::{
24        algebra::Vector2, color::Color, math::Rect, pool::Handle, reflect::prelude::*,
25        some_or_return, type_traits::prelude::*, variable::InheritableVariable,
26        visitor::prelude::*,
27    },
28    draw::{CommandTexture, Draw, DrawingContext},
29    message::{compare_and_set, UiMessage},
30    widget::{Widget, WidgetBuilder},
31    BuildContext, Control, UiNode, UserInterface,
32};
33
34use crate::message::MessageData;
35use fyrox_graph::{
36    constructor::{ConstructorProvider, GraphNodeConstructor},
37    SceneGraph,
38};
39use fyrox_material::MaterialResource;
40use fyrox_texture::{TextureKind, TextureResource};
41use std::ops::DerefMut;
42use strum_macros::{AsRefStr, EnumString, VariantNames};
43
44/// Stretch mode for the middle sections of [`NinePatch`] widget.
45#[derive(
46    Debug,
47    Default,
48    Copy,
49    Clone,
50    Hash,
51    PartialEq,
52    Eq,
53    Reflect,
54    Visit,
55    AsRefStr,
56    EnumString,
57    VariantNames,
58    TypeUuidProvider,
59)]
60#[type_uuid(id = "c5bb0a5c-6581-45f7-899c-78aa1da8b659")]
61pub enum StretchMode {
62    /// Stretches middle sections of the widget. Could lead to distorted image.
63    #[default]
64    Stretch,
65    /// Tiles middle sections of the widget. Prevents distortion of the image.
66    Tile,
67}
68
69/// A set of possible nine patch messages.
70#[derive(Debug, Clone, PartialEq, Eq)]
71pub enum NinePatchMessage {
72    LeftMargin(u32),
73    RightMargin(u32),
74    TopMargin(u32),
75    BottomMargin(u32),
76    TextureRegion(Rect<u32>),
77    Texture(Option<TextureResource>),
78    DrawCenter(bool),
79}
80impl MessageData for NinePatchMessage {}
81
82/// A texture slice that defines a region in a texture and margins that will be used to split the
83/// section in nine pieces.
84#[derive(Default, Clone, Visit, Reflect, Debug, PartialEq)]
85pub struct TextureSlice {
86    /// Texture of the slice. This field is used only for editing purposes in the UI. Can be [`None`]
87    /// if no editing is needed.
88    pub texture_source: Option<TextureResource>,
89    /// Offset from the bottom side of the texture region.
90    pub bottom_margin: InheritableVariable<u32>,
91    /// Offset from the left side of the texture region.
92    pub left_margin: InheritableVariable<u32>,
93    /// Offset from the right side of the texture region.
94    pub right_margin: InheritableVariable<u32>,
95    /// Offset from the top of the texture region.
96    pub top_margin: InheritableVariable<u32>,
97    /// Region in the texture. Default is all zeros, which means that the entire texture is used.
98    pub texture_region: InheritableVariable<Rect<u32>>,
99}
100
101impl TextureSlice {
102    /// Returns the top left point.
103    pub fn margin_min(&self) -> Vector2<u32> {
104        Vector2::new(
105            self.texture_region.position.x + *self.left_margin,
106            self.texture_region.position.y + *self.top_margin,
107        )
108    }
109
110    /// Returns the bottom right point.
111    pub fn margin_max(&self) -> Vector2<u32> {
112        Vector2::new(
113            self.texture_region.position.x
114                + self
115                    .texture_region
116                    .size
117                    .x
118                    .saturating_sub(*self.right_margin),
119            self.texture_region.position.y
120                + self
121                    .texture_region
122                    .size
123                    .y
124                    .saturating_sub(*self.bottom_margin),
125        )
126    }
127}
128
129/// `NinePatch` widget is used to split an image in nine sections, where each corner section will
130/// remain the same, while the middle parts between each corner will be used to evenly fill the
131/// space. This widget is primarily used in the UI to create resizable frames, buttons, windows, etc.
132///
133/// ## Example
134///
135/// The following examples shows how to create a nine-patch widget with a texture and some margins.
136///
137/// ```rust
138/// # use fyrox_ui::{
139/// #     core::{math::Rect, pool::Handle},
140/// #     nine_patch::NinePatchBuilder,
141/// #     widget::WidgetBuilder,
142/// #     UiNode, UserInterface,
143/// # };
144/// # use fyrox_texture::TextureResource;
145/// # use fyrox_ui::nine_patch::NinePatch;
146/// #
147/// fn create_nine_patch(texture: TextureResource, ui: &mut UserInterface) -> Handle<NinePatch> {
148///     NinePatchBuilder::new(WidgetBuilder::new())
149///         // Specify margins for each side in pixels.
150///         .with_left_margin(50)
151///         .with_right_margin(50)
152///         .with_top_margin(40)
153///         .with_bottom_margin(40)
154///         .with_texture(texture)
155///         // Optionally, you can also specify a region in a texture to use. It is useful if you
156///         // have a texture atlas where most of the UI elements are packed.
157///         .with_texture_region(Rect::new(200, 200, 400, 400))
158///         .build(&mut ui.build_ctx())
159/// }
160/// ```
161#[derive(Default, Clone, Visit, Reflect, Debug, ComponentProvider, TypeUuidProvider)]
162#[type_uuid(id = "c345033e-8c10-4186-b101-43f73b85981d")]
163#[reflect(derived_type = "UiNode")]
164pub struct NinePatch {
165    pub widget: Widget,
166    pub texture_slice: TextureSlice,
167    pub draw_center: InheritableVariable<bool>,
168    #[reflect(setter = "set_texture")]
169    pub texture: InheritableVariable<Option<TextureResource>>,
170    pub stretch_mode: InheritableVariable<StretchMode>,
171}
172
173impl NinePatch {
174    pub fn set_texture(&mut self, texture: Option<TextureResource>) {
175        self.texture.set_value_and_mark_modified(texture.clone());
176        self.texture_slice.texture_source = texture;
177    }
178}
179
180impl ConstructorProvider<UiNode, UserInterface> for NinePatch {
181    fn constructor() -> GraphNodeConstructor<UiNode, UserInterface> {
182        GraphNodeConstructor::new::<Self>()
183            .with_variant("Nine Patch", |ui| {
184                NinePatchBuilder::new(
185                    WidgetBuilder::new()
186                        .with_name("Nine Patch")
187                        .with_width(200.0)
188                        .with_height(200.0),
189                )
190                .build(&mut ui.build_ctx())
191                .to_base()
192                .into()
193            })
194            .with_group("Visual")
195    }
196}
197
198crate::define_widget_deref!(NinePatch);
199
200fn draw_image(
201    image: &TextureResource,
202    bounds: Rect<f32>,
203    tex_coords: &[Vector2<f32>; 4],
204    clip_bounds: Rect<f32>,
205    background: Brush,
206    material: &MaterialResource,
207    drawing_context: &mut DrawingContext,
208) {
209    drawing_context.push_rect_filled(&bounds, Some(tex_coords));
210    let texture = CommandTexture::Texture(image.clone());
211    drawing_context.commit(clip_bounds, background, texture, material, None);
212}
213
214fn draw_tiled_image(
215    image: &TextureResource,
216    texture_width: f32,
217    texture_height: f32,
218    bounds: Rect<f32>,
219    tex_coords: &[Vector2<f32>; 4],
220    clip_bounds: Rect<f32>,
221    background: Brush,
222    material: &MaterialResource,
223    drawing_context: &mut DrawingContext,
224) {
225    let region_bounds = Rect::new(
226        tex_coords[0].x * texture_width,
227        tex_coords[0].y * texture_height,
228        (tex_coords[1].x - tex_coords[0].x) * texture_width,
229        (tex_coords[2].y - tex_coords[0].y) * texture_height,
230    );
231
232    let nx = (bounds.size.x / region_bounds.size.x).ceil() as usize;
233    let ny = (bounds.size.y / region_bounds.size.y).ceil() as usize;
234
235    for y in 0..ny {
236        for x in 0..nx {
237            let tile_bounds = Rect::new(
238                bounds.position.x + x as f32 * region_bounds.size.x,
239                bounds.position.y + y as f32 * region_bounds.size.y,
240                region_bounds.size.x,
241                region_bounds.size.y,
242            );
243
244            drawing_context.push_rect_filled(&tile_bounds, Some(tex_coords));
245        }
246    }
247
248    drawing_context.commit(
249        clip_bounds,
250        background,
251        CommandTexture::Texture(image.clone()),
252        material,
253        None,
254    );
255}
256
257impl Control for NinePatch {
258    fn measure_override(&self, ui: &UserInterface, available_size: Vector2<f32>) -> Vector2<f32> {
259        let mut size: Vector2<f32> = available_size;
260
261        let column1_width_pixels = *self.texture_slice.left_margin as f32;
262        let column3_width_pixels = *self.texture_slice.right_margin as f32;
263
264        let row1_height_pixels = *self.texture_slice.top_margin as f32;
265        let row3_height_pixels = *self.texture_slice.bottom_margin as f32;
266
267        let x_overflow = column1_width_pixels + column3_width_pixels;
268        let y_overflow = row1_height_pixels + row3_height_pixels;
269
270        let center_size =
271            Vector2::new(available_size.x - x_overflow, available_size.y - y_overflow);
272
273        for &child in self.children.iter() {
274            ui.measure_node(child, center_size);
275            let desired_size = ui.node(child).desired_size();
276            size.x = size.x.max(desired_size.x.ceil());
277            size.y = size.y.max(desired_size.y.ceil());
278        }
279        size
280    }
281
282    fn arrange_override(&self, ui: &UserInterface, final_size: Vector2<f32>) -> Vector2<f32> {
283        let column1_width_pixels = *self.texture_slice.left_margin as f32;
284        let column3_width_pixels = *self.texture_slice.right_margin as f32;
285
286        let row1_height_pixels = *self.texture_slice.top_margin as f32;
287        let row3_height_pixels = *self.texture_slice.bottom_margin as f32;
288
289        let x_overflow = column1_width_pixels + column3_width_pixels;
290        let y_overflow = row1_height_pixels + row3_height_pixels;
291
292        let final_rect = Rect::new(
293            column1_width_pixels,
294            row1_height_pixels,
295            final_size.x - x_overflow,
296            final_size.y - y_overflow,
297        );
298
299        for &child in self.children.iter() {
300            ui.arrange_node(child, &final_rect);
301        }
302
303        final_size
304    }
305
306    fn draw(&self, drawing_context: &mut DrawingContext) {
307        let texture = some_or_return!(self.texture.as_ref());
308
309        let texture_state = texture.state();
310        let texture_state = some_or_return!(texture_state.data_ref());
311
312        // Only 2D textures can be used with nine-patch.
313        let TextureKind::Rectangle { width, height } = texture_state.kind() else {
314            return;
315        };
316
317        let texture_width = width as f32;
318        let texture_height = height as f32;
319
320        let patch_bounds = self.widget.bounding_rect();
321
322        let left_margin = *self.texture_slice.left_margin as f32;
323        let right_margin = *self.texture_slice.right_margin as f32;
324        let top_margin = *self.texture_slice.top_margin as f32;
325        let bottom_margin = *self.texture_slice.bottom_margin as f32;
326
327        let mut region = Rect {
328            position: self.texture_slice.texture_region.position.cast::<f32>(),
329            size: self.texture_slice.texture_region.size.cast::<f32>(),
330        };
331
332        if region.size.x == 0.0 && region.size.y == 0.0 {
333            region.size.x = texture_width;
334            region.size.y = texture_height;
335        }
336
337        let center_uv_x_min = (region.position.x + left_margin) / texture_width;
338        let center_uv_x_max = (region.position.x + region.size.x - right_margin) / texture_width;
339        let center_uv_y_min = (region.position.y + top_margin) / texture_height;
340        let center_uv_y_max = (region.position.y + region.size.y - bottom_margin) / texture_height;
341        let uv_x_min = region.position.x / texture_width;
342        let uv_x_max = (region.position.x + region.size.x) / texture_width;
343        let uv_y_min = region.position.y / texture_height;
344        let uv_y_max = (region.position.y + region.size.y) / texture_height;
345
346        let x_overflow = left_margin + right_margin;
347        let y_overflow = top_margin + bottom_margin;
348
349        let stretch_mode = *self.stretch_mode;
350        let mut draw_piece = |bounds: Rect<f32>, tex_coords: &[Vector2<f32>; 4]| match stretch_mode
351        {
352            StretchMode::Stretch => {
353                draw_image(
354                    texture,
355                    bounds,
356                    tex_coords,
357                    self.clip_bounds(),
358                    self.widget.background(),
359                    &self.material,
360                    drawing_context,
361                );
362            }
363            StretchMode::Tile => draw_tiled_image(
364                texture,
365                texture_width,
366                texture_height,
367                bounds,
368                tex_coords,
369                self.clip_bounds(),
370                self.widget.background(),
371                &self.material,
372                drawing_context,
373            ),
374        };
375
376        //top left
377        let bounds = Rect {
378            position: patch_bounds.position,
379            size: Vector2::new(left_margin, top_margin),
380        };
381        let tex_coords = [
382            Vector2::new(uv_x_min, uv_y_min),
383            Vector2::new(center_uv_x_min, uv_y_min),
384            Vector2::new(center_uv_x_min, center_uv_y_min),
385            Vector2::new(uv_x_min, center_uv_y_min),
386        ];
387        draw_piece(bounds, &tex_coords);
388
389        //top center
390        let bounds = Rect {
391            position: Vector2::new(
392                patch_bounds.position.x + left_margin,
393                patch_bounds.position.y,
394            ),
395            size: Vector2::new(patch_bounds.size.x - x_overflow, top_margin),
396        };
397        let tex_coords = [
398            Vector2::new(center_uv_x_min, uv_y_min),
399            Vector2::new(center_uv_x_max, uv_y_min),
400            Vector2::new(center_uv_x_max, center_uv_y_min),
401            Vector2::new(center_uv_x_min, center_uv_y_min),
402        ];
403        draw_piece(bounds, &tex_coords);
404
405        //top right
406        let bounds = Rect {
407            position: Vector2::new(
408                (patch_bounds.position.x + patch_bounds.size.x) - right_margin,
409                patch_bounds.position.y,
410            ),
411            size: Vector2::new(right_margin, top_margin),
412        };
413        let tex_coords = [
414            Vector2::new(center_uv_x_max, uv_y_min),
415            Vector2::new(uv_x_max, uv_y_min),
416            Vector2::new(uv_x_max, center_uv_y_min),
417            Vector2::new(center_uv_x_max, center_uv_y_min),
418        ];
419        draw_piece(bounds, &tex_coords);
420        ////////////////////////////////////////////////////////////////////////////////
421        //middle left
422        let bounds = Rect {
423            position: Vector2::new(
424                patch_bounds.position.x,
425                patch_bounds.position.y + top_margin,
426            ),
427            size: Vector2::new(left_margin, patch_bounds.size.y - y_overflow),
428        };
429        let tex_coords = [
430            Vector2::new(uv_x_min, center_uv_y_min),
431            Vector2::new(center_uv_x_min, center_uv_y_min),
432            Vector2::new(center_uv_x_min, center_uv_y_max),
433            Vector2::new(uv_x_min, center_uv_y_max),
434        ];
435        draw_piece(bounds, &tex_coords);
436
437        if *self.draw_center {
438            //middle center
439            let bounds = Rect {
440                position: Vector2::new(
441                    patch_bounds.position.x + left_margin,
442                    patch_bounds.position.y + top_margin,
443                ),
444                size: Vector2::new(
445                    patch_bounds.size.x - x_overflow,
446                    patch_bounds.size.y - y_overflow,
447                ),
448            };
449            let tex_coords = [
450                Vector2::new(center_uv_x_min, center_uv_y_min),
451                Vector2::new(center_uv_x_max, center_uv_y_min),
452                Vector2::new(center_uv_x_max, center_uv_y_max),
453                Vector2::new(center_uv_x_min, center_uv_y_max),
454            ];
455            draw_piece(bounds, &tex_coords);
456        }
457
458        //middle right
459        let bounds = Rect {
460            position: Vector2::new(
461                (patch_bounds.position.x + patch_bounds.size.x) - right_margin,
462                patch_bounds.position.y + top_margin,
463            ),
464            size: Vector2::new(right_margin, patch_bounds.size.y - y_overflow),
465        };
466        let tex_coords = [
467            Vector2::new(center_uv_x_max, center_uv_y_min),
468            Vector2::new(uv_x_max, center_uv_y_min),
469            Vector2::new(uv_x_max, center_uv_y_max),
470            Vector2::new(center_uv_x_max, center_uv_y_max),
471        ];
472        draw_piece(bounds, &tex_coords);
473
474        ////////////////////////////////////////////////////////////////////////////////
475        //bottom left
476        let bounds = Rect {
477            position: Vector2::new(
478                patch_bounds.position.x,
479                (patch_bounds.position.y + patch_bounds.size.y) - bottom_margin,
480            ),
481            size: Vector2::new(left_margin, bottom_margin),
482        };
483        let tex_coords = [
484            Vector2::new(uv_x_min, center_uv_y_max),
485            Vector2::new(center_uv_x_min, center_uv_y_max),
486            Vector2::new(center_uv_x_min, uv_y_max),
487            Vector2::new(uv_x_min, uv_y_max),
488        ];
489        draw_piece(bounds, &tex_coords);
490
491        //bottom center
492        let bounds = Rect {
493            position: Vector2::new(
494                patch_bounds.position.x + left_margin,
495                (patch_bounds.position.y + patch_bounds.size.y) - bottom_margin,
496            ),
497            size: Vector2::new(patch_bounds.size.x - x_overflow, bottom_margin),
498        };
499        let tex_coords = [
500            Vector2::new(center_uv_x_min, center_uv_y_max),
501            Vector2::new(center_uv_x_max, center_uv_y_max),
502            Vector2::new(center_uv_x_max, uv_y_max),
503            Vector2::new(center_uv_x_min, uv_y_max),
504        ];
505        draw_piece(bounds, &tex_coords);
506
507        //bottom right
508        let bounds = Rect {
509            position: Vector2::new(
510                (patch_bounds.position.x + patch_bounds.size.x) - right_margin,
511                (patch_bounds.position.y + patch_bounds.size.y) - bottom_margin,
512            ),
513            size: Vector2::new(right_margin, bottom_margin),
514        };
515        let tex_coords = [
516            Vector2::new(center_uv_x_max, center_uv_y_max),
517            Vector2::new(uv_x_max, center_uv_y_max),
518            Vector2::new(uv_x_max, uv_y_max),
519            Vector2::new(center_uv_x_max, uv_y_max),
520        ];
521        draw_piece(bounds, &tex_coords);
522
523        //end drawing
524    }
525
526    fn handle_routed_message(&mut self, ui: &mut UserInterface, message: &mut UiMessage) {
527        self.widget.handle_routed_message(ui, message);
528
529        if let Some(msg) = message.data_for::<NinePatchMessage>(self.handle) {
530            let slice = &mut self.texture_slice;
531            match msg {
532                NinePatchMessage::LeftMargin(margin) => {
533                    compare_and_set(slice.left_margin.deref_mut(), margin, message, ui);
534                    self.invalidate_visual();
535                }
536                NinePatchMessage::RightMargin(margin) => {
537                    compare_and_set(slice.right_margin.deref_mut(), margin, message, ui);
538                    self.invalidate_visual();
539                }
540                NinePatchMessage::TopMargin(margin) => {
541                    compare_and_set(slice.top_margin.deref_mut(), margin, message, ui);
542                    self.invalidate_visual();
543                }
544                NinePatchMessage::BottomMargin(margin) => {
545                    compare_and_set(slice.bottom_margin.deref_mut(), margin, message, ui);
546                    self.invalidate_visual();
547                }
548                NinePatchMessage::TextureRegion(region) => {
549                    compare_and_set(slice.texture_region.deref_mut(), region, message, ui);
550                    self.invalidate_visual();
551                }
552                NinePatchMessage::Texture(texture) => {
553                    compare_and_set(&mut slice.texture_source, texture, message, ui);
554                    self.invalidate_visual();
555                }
556                NinePatchMessage::DrawCenter(draw_center) => {
557                    compare_and_set(self.draw_center.deref_mut(), draw_center, message, ui);
558                    self.invalidate_visual();
559                }
560            }
561        }
562    }
563}
564
565/// Creates instances of [`NinePatch`] widget.
566pub struct NinePatchBuilder {
567    pub widget_builder: WidgetBuilder,
568    pub texture: Option<TextureResource>,
569    pub bottom_margin: u32,
570    pub left_margin: u32,
571    pub right_margin: u32,
572    pub top_margin: u32,
573    pub texture_region: Rect<u32>,
574    pub draw_center: bool,
575    pub stretch_mode: StretchMode,
576}
577
578impl NinePatchBuilder {
579    pub fn new(widget_builder: WidgetBuilder) -> Self {
580        Self {
581            widget_builder,
582            texture: None,
583            bottom_margin: 20,
584            left_margin: 20,
585            right_margin: 20,
586            top_margin: 20,
587            texture_region: Rect::new(0, 0, 200, 200),
588            draw_center: true,
589            stretch_mode: Default::default(),
590        }
591    }
592
593    pub fn with_texture(mut self, texture: TextureResource) -> Self {
594        self.texture = Some(texture);
595        self
596    }
597
598    pub fn with_bottom_margin(mut self, margin: u32) -> Self {
599        self.bottom_margin = margin;
600        self
601    }
602
603    pub fn with_left_margin(mut self, margin: u32) -> Self {
604        self.left_margin = margin;
605        self
606    }
607
608    pub fn with_right_margin(mut self, margin: u32) -> Self {
609        self.right_margin = margin;
610        self
611    }
612
613    pub fn with_top_margin(mut self, margin: u32) -> Self {
614        self.top_margin = margin;
615        self
616    }
617
618    pub fn with_texture_region(mut self, rect: Rect<u32>) -> Self {
619        self.texture_region = rect;
620        self
621    }
622
623    pub fn with_draw_center(mut self, draw_center: bool) -> Self {
624        self.draw_center = draw_center;
625        self
626    }
627
628    pub fn with_stretch_mode(mut self, stretch: StretchMode) -> Self {
629        self.stretch_mode = stretch;
630        self
631    }
632
633    pub fn build(mut self, ctx: &mut BuildContext) -> Handle<NinePatch> {
634        if self.widget_builder.background.is_none() {
635            self.widget_builder.background = Some(Brush::Solid(Color::WHITE).into())
636        }
637
638        ctx.add(NinePatch {
639            widget: self.widget_builder.build(ctx),
640            texture_slice: TextureSlice {
641                texture_source: self.texture.clone(),
642                bottom_margin: self.bottom_margin.into(),
643                left_margin: self.left_margin.into(),
644                right_margin: self.right_margin.into(),
645                top_margin: self.top_margin.into(),
646                texture_region: self.texture_region.into(),
647            },
648            draw_center: self.draw_center.into(),
649            texture: self.texture.into(),
650            stretch_mode: self.stretch_mode.into(),
651        })
652    }
653}
654
655#[cfg(test)]
656mod test {
657    use crate::nine_patch::NinePatchBuilder;
658    use crate::{test::test_widget_deletion, widget::WidgetBuilder};
659
660    #[test]
661    fn test_deletion() {
662        test_widget_deletion(|ctx| NinePatchBuilder::new(WidgetBuilder::new()).build(ctx));
663    }
664}