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}