fyrox_ui/
image.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
21//! Image widget is a rectangle with a texture, it is used draw custom bitmaps. See [`Image`] docs for more info
22//! and usage examples.
23
24#![warn(missing_docs)]
25
26use crate::{
27    brush::Brush,
28    color::draw_checker_board,
29    core::{
30        algebra::Vector2, color::Color, math::Rect, pool::Handle, reflect::prelude::*,
31        type_traits::prelude::*, variable::InheritableVariable, visitor::prelude::*,
32    },
33    define_constructor,
34    draw::{CommandTexture, Draw, DrawingContext},
35    message::{MessageDirection, UiMessage},
36    widget::{Widget, WidgetBuilder},
37    BuildContext, Control, UiNode, UserInterface,
38};
39
40use fyrox_graph::constructor::{ConstructorProvider, GraphNodeConstructor};
41use fyrox_texture::{TextureKind, TextureResource};
42use std::ops::{Deref, DerefMut};
43
44/// A set of messages that could be used to alter [`Image`] widget state at runtime.
45#[derive(Debug, Clone, PartialEq)]
46pub enum ImageMessage {
47    /// Used to set new texture of the [`Image`] widget.
48    Texture(Option<TextureResource>),
49    /// Used to enable or disable texture flip of the [`Image`] widget. See respective [section](Image#vertical-flip)
50    /// of the docs for more info.
51    Flip(bool),
52    /// Used to set specific portion of the texture. See respective [section](Image#drawing-only-a-portion-of-the-texture)
53    /// of the docs for more info.
54    UvRect(Rect<f32>),
55    /// Used to enable or disable checkerboard background. See respective [section](Image#checkerboard-background) of the
56    /// docs for more info.
57    CheckerboardBackground(bool),
58}
59
60impl ImageMessage {
61    define_constructor!(
62        /// Creates [`ImageMessage::Texture`] message.
63        ImageMessage:Texture => fn texture(Option<TextureResource>), layout: false
64    );
65
66    define_constructor!(
67        /// Creates [`ImageMessage::Flip`] message.
68        ImageMessage:Flip => fn flip(bool), layout: false
69    );
70
71    define_constructor!(
72        /// Creates [`ImageMessage::UvRect`] message.
73        ImageMessage:UvRect => fn uv_rect(Rect<f32>), layout: false
74    );
75
76    define_constructor!(
77        /// Creates [`ImageMessage::CheckerboardBackground`] message.
78        ImageMessage:CheckerboardBackground => fn checkerboard_background(bool), layout: false
79    );
80}
81
82/// Image widget is a rectangle with a texture, it is used draw custom bitmaps. The UI in the engine is vector-based, Image
83/// widget is the only way to draw a bitmap. Usage of the Image is very simple:
84///
85/// ## Usage
86///
87/// ```rust,no_run
88/// # use fyrox_texture::TextureResource;
89/// # use fyrox_ui::{
90/// #     core::pool::Handle,
91/// #     image::ImageBuilder, widget::WidgetBuilder, BuildContext, UiNode,
92/// # };
93///
94/// fn create_image(ctx: &mut BuildContext, texture: TextureResource) -> Handle<UiNode> {
95///     ImageBuilder::new(WidgetBuilder::new())
96///         .with_texture(texture)
97///         .build(ctx)
98/// }
99/// ```
100///
101/// By default, the Image widget will try to use the size of the texture as its desired size for layout
102/// process. This means that the widget will be as large as the texture if the outer bounds allows
103/// that. You can specify the desired width and height manually and the image will shrink/expand
104/// automatically.
105///
106/// Keep in mind, that texture is a resource, and it could be loaded asynchronously, and during that
107/// process, the UI can't fetch texture's size, and it will be collapsed into a point. After it fully
108/// loaded, the widget will take texture's size as normal.
109///
110/// ## Vertical Flip
111///
112/// In some rare cases you need to flip your source image before showing it, there is `.with_flip` option for that:
113///
114/// ```rust,no_run
115/// # use fyrox_texture::TextureResource;
116/// # use fyrox_ui::{
117/// #     core::pool::Handle,
118/// #     image::ImageBuilder, widget::WidgetBuilder, BuildContext, UiNode
119/// # };
120///
121/// fn create_image(ctx: &mut BuildContext, texture: TextureResource) -> Handle<UiNode> {
122///     ImageBuilder::new(WidgetBuilder::new().with_width(100.0).with_height(100.0))
123///         .with_flip(true) // Flips an image vertically
124///         .with_texture(texture)
125///         .build(ctx)
126/// }
127/// ```
128///
129/// There are few places where it can be helpful:
130///
131/// - You're using render target as a source texture for your [`Image`] instance, render targets are vertically flipped due
132/// to mismatch of coordinates of UI and graphics API. The UI has origin at left top corner, the graphics API - bottom left.
133/// - Your source image is vertically mirrored.
134///
135/// ## Checkerboard background
136///
137/// Image widget supports checkerboard background that could be useful for images with alpha channel (transparency). It can
138/// be enabled either when building the widget or via [`ImageMessage::CheckerboardBackground`] message:
139///
140/// ```rust,no_run
141/// # use fyrox_texture::TextureResource;
142/// # use fyrox_ui::{
143/// #     core::pool::Handle,
144/// #     image::ImageBuilder, widget::WidgetBuilder, BuildContext, UiNode
145/// # };
146///
147/// fn create_image(ctx: &mut BuildContext, texture: TextureResource) -> Handle<UiNode> {
148///     ImageBuilder::new(WidgetBuilder::new().with_width(100.0).with_height(100.0))
149///         .with_checkerboard_background(true) // Turns on checkerboard background.
150///         .with_texture(texture)
151///         .build(ctx)
152/// }
153/// ```
154///
155/// ## Drawing only a portion of the texture
156///
157/// Specific cases requires to be able to draw a specific rectangular portion of the texture. It could be done by using
158/// custom UV rect (UV stands for XY coordinates, but texture related):
159///
160/// ```rust,no_run
161/// # use fyrox_texture::TextureResource;
162/// # use fyrox_ui::{
163/// #     core::{pool::Handle, math::Rect},
164/// #     image::ImageBuilder, widget::WidgetBuilder, BuildContext, UiNode
165/// # };
166///
167/// fn create_image(ctx: &mut BuildContext, texture: TextureResource) -> Handle<UiNode> {
168///     ImageBuilder::new(WidgetBuilder::new().with_width(100.0).with_height(100.0))
169///         .with_uv_rect(Rect::new(0.0, 0.0, 0.25, 0.25)) // Uses top-left quadrant of the texture.
170///         .with_texture(texture)
171///         .build(ctx)
172/// }
173/// ```
174///
175/// Keep in mind, that the rectangle uses _normalized_ coordinates. This means that the entire image dimensions (for both
176/// X and Y axes) "compressed" to `0.0..1.0` range. In this case 0.0 means left corner for X axis and top for Y axis, while
177/// 1.0 means right corner for X axis and bottom for Y axis.
178///
179/// It is useful if you have many custom UI elements packed in a single texture atlas. Drawing using atlases is much more
180/// efficient and faster. This could also be used for animations, when you have multiple frames packed in a single atlas
181/// and changing texture coordinates over the time.
182#[derive(Default, Clone, Visit, Reflect, Debug, ComponentProvider, TypeUuidProvider)]
183#[type_uuid(id = "18e18d0f-cb84-4ac1-8050-3480a2ec3de5")]
184#[visit(optional)]
185#[reflect(derived_type = "UiNode")]
186pub struct Image {
187    /// Base widget of the image.
188    pub widget: Widget,
189    /// Current texture of the image.
190    pub texture: InheritableVariable<Option<TextureResource>>,
191    /// Defines whether to vertically flip the image or not.
192    pub flip: InheritableVariable<bool>,
193    /// Specifies an arbitrary portion of the texture.
194    pub uv_rect: InheritableVariable<Rect<f32>>,
195    /// Defines whether to use the checkerboard background or not.
196    pub checkerboard_background: InheritableVariable<bool>,
197    /// Defines whether the image should keep its aspect ratio or stretch to the available size.
198    pub keep_aspect_ratio: InheritableVariable<bool>,
199    /// Defines whether the image should keep its size in sync with the size of an assigned texture.
200    pub sync_with_texture_size: InheritableVariable<bool>,
201}
202
203impl ConstructorProvider<UiNode, UserInterface> for Image {
204    fn constructor() -> GraphNodeConstructor<UiNode, UserInterface> {
205        GraphNodeConstructor::new::<Self>()
206            .with_variant("Image", |ui| {
207                ImageBuilder::new(
208                    WidgetBuilder::new()
209                        .with_height(32.0)
210                        .with_width(32.0)
211                        .with_name("Image"),
212                )
213                .build(&mut ui.build_ctx())
214                .into()
215            })
216            .with_group("Visual")
217    }
218}
219
220crate::define_widget_deref!(Image);
221
222impl Control for Image {
223    fn measure_override(&self, ui: &UserInterface, available_size: Vector2<f32>) -> Vector2<f32> {
224        let mut size: Vector2<f32> = self.widget.measure_override(ui, available_size);
225
226        if *self.sync_with_texture_size {
227            if let Some(texture) = self.texture.as_ref() {
228                let state = texture.state();
229                if let Some(data) = state.data_ref() {
230                    if let TextureKind::Rectangle { width, height } = data.kind() {
231                        let width = width as f32;
232                        let height = height as f32;
233
234                        if *self.keep_aspect_ratio {
235                            let aspect_ratio = width / height;
236                            size.x = size.x.max(width).min(available_size.x);
237                            size.y = size.x * aspect_ratio;
238                        } else {
239                            size.x = size.x.max(width);
240                            size.y = size.y.max(height);
241                        }
242                    }
243                }
244            }
245        }
246
247        size
248    }
249
250    fn draw(&self, drawing_context: &mut DrawingContext) {
251        let bounds = self.widget.bounding_rect();
252
253        if *self.checkerboard_background {
254            draw_checker_board(
255                bounds,
256                self.clip_bounds(),
257                8.0,
258                &self.material,
259                drawing_context,
260            );
261        }
262
263        if self.texture.is_some() || !*self.checkerboard_background {
264            let tex_coords = if *self.flip {
265                Some([
266                    Vector2::new(self.uv_rect.position.x, self.uv_rect.position.y),
267                    Vector2::new(
268                        self.uv_rect.position.x + self.uv_rect.size.x,
269                        self.uv_rect.position.y,
270                    ),
271                    Vector2::new(
272                        self.uv_rect.position.x + self.uv_rect.size.x,
273                        self.uv_rect.position.y - self.uv_rect.size.y,
274                    ),
275                    Vector2::new(
276                        self.uv_rect.position.x,
277                        self.uv_rect.position.y - self.uv_rect.size.y,
278                    ),
279                ])
280            } else {
281                Some([
282                    Vector2::new(self.uv_rect.position.x, self.uv_rect.position.y),
283                    Vector2::new(
284                        self.uv_rect.position.x + self.uv_rect.size.x,
285                        self.uv_rect.position.y,
286                    ),
287                    Vector2::new(
288                        self.uv_rect.position.x + self.uv_rect.size.x,
289                        self.uv_rect.position.y + self.uv_rect.size.y,
290                    ),
291                    Vector2::new(
292                        self.uv_rect.position.x,
293                        self.uv_rect.position.y + self.uv_rect.size.y,
294                    ),
295                ])
296            };
297            drawing_context.push_rect_filled(&bounds, tex_coords.as_ref());
298            let texture = self
299                .texture
300                .as_ref()
301                .map_or(CommandTexture::None, |t| CommandTexture::Texture(t.clone()));
302            drawing_context.commit(
303                self.clip_bounds(),
304                self.widget.background(),
305                texture,
306                &self.material,
307                None,
308            );
309        }
310    }
311
312    fn handle_routed_message(&mut self, ui: &mut UserInterface, message: &mut UiMessage) {
313        self.widget.handle_routed_message(ui, message);
314
315        if let Some(msg) = message.data::<ImageMessage>() {
316            if message.destination() == self.handle {
317                match msg {
318                    ImageMessage::Texture(tex) => {
319                        self.texture.set_value_and_mark_modified(tex.clone());
320                    }
321                    &ImageMessage::Flip(flip) => {
322                        self.flip.set_value_and_mark_modified(flip);
323                    }
324                    ImageMessage::UvRect(uv_rect) => {
325                        self.uv_rect.set_value_and_mark_modified(*uv_rect);
326                    }
327                    ImageMessage::CheckerboardBackground(value) => {
328                        self.checkerboard_background
329                            .set_value_and_mark_modified(*value);
330                    }
331                }
332            }
333        }
334    }
335}
336
337/// Image builder is used to create [`Image`] widget instances and register them in the user interface.
338pub struct ImageBuilder {
339    widget_builder: WidgetBuilder,
340    texture: Option<TextureResource>,
341    flip: bool,
342    uv_rect: Rect<f32>,
343    checkerboard_background: bool,
344    keep_aspect_ratio: bool,
345    sync_with_texture_size: bool,
346}
347
348impl ImageBuilder {
349    /// Creates new image builder with the base widget builder specified.
350    pub fn new(widget_builder: WidgetBuilder) -> Self {
351        Self {
352            widget_builder,
353            texture: None,
354            flip: false,
355            uv_rect: Rect::new(0.0, 0.0, 1.0, 1.0),
356            checkerboard_background: false,
357            keep_aspect_ratio: true,
358            sync_with_texture_size: true,
359        }
360    }
361
362    /// Sets whether the image should be flipped vertically or not. See respective
363    /// [section](Image#vertical-flip) of the docs for more info.
364    pub fn with_flip(mut self, flip: bool) -> Self {
365        self.flip = flip;
366        self
367    }
368
369    /// Sets the texture that will be used for drawing.
370    pub fn with_texture(mut self, texture: TextureResource) -> Self {
371        self.texture = Some(texture);
372        self
373    }
374
375    /// Specifies the texture that will be used for drawing.
376    pub fn with_opt_texture(mut self, texture: Option<TextureResource>) -> Self {
377        self.texture = texture;
378        self
379    }
380
381    /// Specifies a portion of the texture in normalized coordinates. See respective
382    /// [section](Image#drawing-only-a-portion-of-the-texture) of the docs for more info.
383    pub fn with_uv_rect(mut self, uv_rect: Rect<f32>) -> Self {
384        self.uv_rect = uv_rect;
385        self
386    }
387
388    /// Sets whether the image should use checkerboard background or not. See respective
389    /// [section](Image#checkerboard-background) of the docs for more info.
390    pub fn with_checkerboard_background(mut self, checkerboard_background: bool) -> Self {
391        self.checkerboard_background = checkerboard_background;
392        self
393    }
394
395    /// Sets whether the image should keep its aspect ratio or stretch to the available size.
396    pub fn with_keep_aspect_ratio(mut self, keep_aspect_ratio: bool) -> Self {
397        self.keep_aspect_ratio = keep_aspect_ratio;
398        self
399    }
400
401    /// Sets whether the image should keep its size in sync with the size of an assigned texture.
402    pub fn with_sync_with_texture_size(mut self, sync_with_texture_size: bool) -> Self {
403        self.sync_with_texture_size = sync_with_texture_size;
404        self
405    }
406
407    /// Builds the [`Image`] widget, but does not add it to the UI.
408    pub fn build_node(mut self, ctx: &BuildContext) -> UiNode {
409        if self.widget_builder.background.is_none() {
410            self.widget_builder.background = Some(Brush::Solid(Color::WHITE).into())
411        }
412
413        let image = Image {
414            widget: self.widget_builder.build(ctx),
415            texture: self.texture.into(),
416            flip: self.flip.into(),
417            uv_rect: self.uv_rect.into(),
418            checkerboard_background: self.checkerboard_background.into(),
419            keep_aspect_ratio: self.keep_aspect_ratio.into(),
420            sync_with_texture_size: self.sync_with_texture_size.into(),
421        };
422        UiNode::new(image)
423    }
424
425    /// Builds the [`Image`] widget and adds it to the UI and returns its handle.
426    pub fn build(self, ctx: &mut BuildContext) -> Handle<UiNode> {
427        ctx.add_node(self.build_node(ctx))
428    }
429}
430
431#[cfg(test)]
432mod test {
433    use crate::image::ImageBuilder;
434    use crate::{test::test_widget_deletion, widget::WidgetBuilder};
435
436    #[test]
437    fn test_deletion() {
438        test_widget_deletion(|ctx| ImageBuilder::new(WidgetBuilder::new()).build(ctx));
439    }
440}