1use 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#[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 #[default]
64 Stretch,
65 Tile,
67}
68
69#[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#[derive(Default, Clone, Visit, Reflect, Debug, PartialEq)]
85pub struct TextureSlice {
86 pub texture_source: Option<TextureResource>,
89 pub bottom_margin: InheritableVariable<u32>,
91 pub left_margin: InheritableVariable<u32>,
93 pub right_margin: InheritableVariable<u32>,
95 pub top_margin: InheritableVariable<u32>,
97 pub texture_region: InheritableVariable<Rect<u32>>,
99}
100
101impl TextureSlice {
102 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 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#[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 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 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 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 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 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 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 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 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 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 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 }
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
565pub 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}