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