Skip to main content

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