1pub mod accessibility;
2#[cfg(all(feature = "a11y", target_arch = "wasm32"))]
3pub mod accessibility_web;
4#[cfg(all(feature = "a11y", not(target_arch = "wasm32")))]
5pub mod accessibility_native;
6pub mod align;
7pub mod color;
8pub mod elements;
9pub mod engine;
10pub mod id;
11pub mod layout;
12pub mod math;
13pub mod render_commands;
14pub mod shader_build;
15pub mod shaders;
16pub mod text;
17pub mod text_input;
18pub mod renderer;
19#[cfg(feature = "text-styling")]
20pub mod text_styling;
21#[cfg(feature = "built-in-shaders")]
22pub mod built_in_shaders;
23#[cfg(feature = "net")]
24pub mod net;
25pub mod prelude;
26
27use id::Id;
28use math::{Dimensions, Vector2};
29use render_commands::RenderCommand;
30use text::TextConfig;
31
32pub use color::Color;
33
34#[allow(dead_code)]
35pub struct Ply<CustomElementData: Clone + Default + std::fmt::Debug = ()> {
36 context: engine::PlyContext<CustomElementData>,
37 headless: bool,
38 text_input_repeat_key: u32,
40 text_input_repeat_first: f64,
41 text_input_repeat_last: f64,
42 text_input_repeat_focus_id: u32,
45 was_text_input_focused: bool,
47 #[cfg(all(feature = "a11y", target_arch = "wasm32"))]
48 web_a11y_state: accessibility_web::WebAccessibilityState,
49 #[cfg(all(feature = "a11y", not(target_arch = "wasm32")))]
50 native_a11y_state: accessibility_native::NativeAccessibilityState,
51}
52
53pub struct Ui<'ply, CustomElementData: Clone + Default + std::fmt::Debug = ()> {
54 ply: &'ply mut Ply<CustomElementData>,
55}
56
57pub struct ElementBuilder<'ply, CustomElementData: Clone + Default + std::fmt::Debug = ()> {
60 ply: &'ply mut Ply<CustomElementData>,
61 inner: engine::ElementDeclaration<CustomElementData>,
62 id: Option<Id>,
63 on_hover_fn: Option<Box<dyn FnMut(Id, engine::PointerData) + 'static>>,
64 on_press_fn: Option<Box<dyn FnMut(Id, engine::PointerData) + 'static>>,
65 on_release_fn: Option<Box<dyn FnMut(Id, engine::PointerData) + 'static>>,
66 on_focus_fn: Option<Box<dyn FnMut(Id) + 'static>>,
67 on_unfocus_fn: Option<Box<dyn FnMut(Id) + 'static>>,
68 text_input_on_changed_fn: Option<Box<dyn FnMut(&str) + 'static>>,
69 text_input_on_submit_fn: Option<Box<dyn FnMut(&str) + 'static>>,
70}
71
72impl<'ply, CustomElementData: Clone + Default + std::fmt::Debug>
73 ElementBuilder<'ply, CustomElementData>
74{
75 #[inline]
77 pub fn width(mut self, width: layout::Sizing) -> Self {
78 self.inner.layout.sizing.width = width.into();
79 self
80 }
81
82 #[inline]
84 pub fn height(mut self, height: layout::Sizing) -> Self {
85 self.inner.layout.sizing.height = height.into();
86 self
87 }
88
89 #[inline]
91 pub fn background_color(mut self, color: impl Into<Color>) -> Self {
92 self.inner.background_color = color.into();
93 self
94 }
95
96 #[inline]
99 pub fn corner_radius(mut self, radius: impl Into<layout::CornerRadius>) -> Self {
100 self.inner.corner_radius = radius.into();
101 self
102 }
103
104 #[inline]
108 pub fn id(mut self, id: impl Into<Id>) -> Self {
109 self.id = Some(id.into());
110 self
111 }
112
113 #[inline]
115 pub fn aspect_ratio(mut self, aspect_ratio: f32) -> Self {
116 self.inner.aspect_ratio = aspect_ratio;
117 self
118 }
119
120 #[inline]
122 pub fn overflow(mut self, f: impl for<'a> FnOnce(&'a mut elements::OverflowBuilder) -> &'a mut elements::OverflowBuilder) -> Self {
123 let mut builder = elements::OverflowBuilder { config: self.inner.clip };
124 f(&mut builder);
125 self.inner.clip = builder.config;
126 self
127 }
128
129 #[inline]
131 pub fn custom_element(mut self, data: CustomElementData) -> Self {
132 self.inner.custom_data = Some(data);
133 self
134 }
135
136 #[inline]
138 pub fn layout(mut self, f: impl for<'a> FnOnce(&'a mut layout::LayoutBuilder) -> &'a mut layout::LayoutBuilder) -> Self {
139 let mut builder = layout::LayoutBuilder { config: self.inner.layout };
140 f(&mut builder);
141 self.inner.layout = builder.config;
142 self
143 }
144
145 #[inline]
147 pub fn floating(mut self, f: impl for<'a> FnOnce(&'a mut elements::FloatingBuilder) -> &'a mut elements::FloatingBuilder) -> Self {
148 let mut builder = elements::FloatingBuilder { config: self.inner.floating };
149 f(&mut builder);
150 self.inner.floating = builder.config;
151 self
152 }
153
154 #[inline]
156 pub fn border(mut self, f: impl for<'a> FnOnce(&'a mut elements::BorderBuilder) -> &'a mut elements::BorderBuilder) -> Self {
157 let mut builder = elements::BorderBuilder { config: self.inner.border };
158 f(&mut builder);
159 self.inner.border = builder.config;
160 self
161 }
162
163 #[inline]
170 pub fn image(mut self, data: impl Into<renderer::ImageSource>) -> Self {
171 self.inner.image_data = Some(data.into());
172 self
173 }
174
175 #[inline]
190 pub fn effect(mut self, asset: &shaders::ShaderAsset, f: impl FnOnce(&mut shaders::ShaderBuilder<'_>)) -> Self {
191 let mut builder = shaders::ShaderBuilder::new(asset);
192 f(&mut builder);
193 self.inner.effects.push(builder.into_config());
194 self
195 }
196
197 #[inline]
216 pub fn shader(mut self, asset: &shaders::ShaderAsset, f: impl FnOnce(&mut shaders::ShaderBuilder<'_>)) -> Self {
217 let mut builder = shaders::ShaderBuilder::new(asset);
218 f(&mut builder);
219 self.inner.shaders.push(builder.into_config());
220 self
221 }
222
223 #[inline]
244 pub fn rotate_visual(mut self, f: impl for<'a> FnOnce(&'a mut elements::VisualRotationBuilder) -> &'a mut elements::VisualRotationBuilder) -> Self {
245 let mut builder = elements::VisualRotationBuilder {
246 config: engine::VisualRotationConfig::default(),
247 };
248 f(&mut builder);
249 self.inner.visual_rotation = Some(builder.config);
250 self
251 }
252
253 #[inline]
269 pub fn rotate_shape(mut self, f: impl for<'a> FnOnce(&'a mut elements::ShapeRotationBuilder) -> &'a mut elements::ShapeRotationBuilder) -> Self {
270 let mut builder = elements::ShapeRotationBuilder {
271 config: engine::ShapeRotationConfig::default(),
272 };
273 f(&mut builder);
274 self.inner.shape_rotation = Some(builder.config);
275 self
276 }
277
278 #[inline]
291 pub fn accessibility(
292 mut self,
293 f: impl for<'a> FnOnce(&'a mut accessibility::AccessibilityBuilder) -> &'a mut accessibility::AccessibilityBuilder,
294 ) -> Self {
295 let mut builder = accessibility::AccessibilityBuilder::new();
296 f(&mut builder);
297 self.inner.accessibility = Some(builder.config);
298 self
299 }
300
301 #[inline]
304 pub fn preserve_focus(mut self) -> Self {
305 self.inner.preserve_focus = true;
306 self
307 }
308
309 #[inline]
311 pub fn on_hover<F>(mut self, callback: F) -> Self
312 where
313 F: FnMut(Id, engine::PointerData) + 'static,
314 {
315 self.on_hover_fn = Some(Box::new(callback));
316 self
317 }
318
319 #[inline]
322 pub fn on_press<F>(mut self, callback: F) -> Self
323 where
324 F: FnMut(Id, engine::PointerData) + 'static,
325 {
326 self.on_press_fn = Some(Box::new(callback));
327 self
328 }
329
330 #[inline]
333 pub fn on_release<F>(mut self, callback: F) -> Self
334 where
335 F: FnMut(Id, engine::PointerData) + 'static,
336 {
337 self.on_release_fn = Some(Box::new(callback));
338 self
339 }
340
341 #[inline]
344 pub fn on_focus<F>(mut self, callback: F) -> Self
345 where
346 F: FnMut(Id) + 'static,
347 {
348 self.on_focus_fn = Some(Box::new(callback));
349 self
350 }
351
352 #[inline]
354 pub fn on_unfocus<F>(mut self, callback: F) -> Self
355 where
356 F: FnMut(Id) + 'static,
357 {
358 self.on_unfocus_fn = Some(Box::new(callback));
359 self
360 }
361
362 #[inline]
381 pub fn text_input(
382 mut self,
383 f: impl for<'a> FnOnce(&'a mut text_input::TextInputBuilder) -> &'a mut text_input::TextInputBuilder,
384 ) -> Self {
385 let mut builder = text_input::TextInputBuilder::new();
386 f(&mut builder);
387 self.inner.text_input = Some(builder.config);
388 self.text_input_on_changed_fn = builder.on_changed_fn;
389 self.text_input_on_submit_fn = builder.on_submit_fn;
390 self
391 }
392
393 pub fn children(self, f: impl FnOnce(&mut Ui<'_, CustomElementData>)) -> Id {
395 let ElementBuilder {
396 ply, inner, id,
397 on_hover_fn, on_press_fn, on_release_fn, on_focus_fn, on_unfocus_fn,
398 text_input_on_changed_fn, text_input_on_submit_fn,
399 } = self;
400 if let Some(ref id) = id {
401 ply.context.open_element_with_id(id);
402 } else {
403 ply.context.open_element();
404 }
405 ply.context.configure_open_element(&inner);
406 let element_id = ply.context.get_open_element_id();
407
408 if let Some(hover_fn) = on_hover_fn {
409 ply.context.on_hover(hover_fn);
410 }
411 if on_press_fn.is_some() || on_release_fn.is_some() {
412 ply.context.set_press_callbacks(on_press_fn, on_release_fn);
413 }
414 if on_focus_fn.is_some() || on_unfocus_fn.is_some() {
415 ply.context.set_focus_callbacks(on_focus_fn, on_unfocus_fn);
416 }
417 if text_input_on_changed_fn.is_some() || text_input_on_submit_fn.is_some() {
418 ply.context.set_text_input_callbacks(text_input_on_changed_fn, text_input_on_submit_fn);
419 }
420
421 let mut ui = Ui { ply };
422 f(&mut ui);
423 ui.ply.context.close_element();
424
425 Id { id: element_id, ..Default::default() }
426 }
427
428 pub fn empty(self) -> Id {
430 self.children(|_| {})
431 }
432}
433
434impl<'ply, CustomElementData: Clone + Default + std::fmt::Debug> core::ops::Deref
435 for Ui<'ply, CustomElementData>
436{
437 type Target = Ply<CustomElementData>;
438
439 fn deref(&self) -> &Self::Target {
440 self.ply
441 }
442}
443
444impl<'ply, CustomElementData: Clone + Default + std::fmt::Debug> core::ops::DerefMut
445 for Ui<'ply, CustomElementData>
446{
447 fn deref_mut(&mut self) -> &mut Self::Target {
448 self.ply
449 }
450}
451
452impl<'ply, CustomElementData: Clone + Default + std::fmt::Debug> Ui<'ply, CustomElementData> {
453 pub fn element(&mut self) -> ElementBuilder<'_, CustomElementData> {
456 ElementBuilder {
457 ply: &mut *self.ply,
458 inner: engine::ElementDeclaration::default(),
459 id: None,
460 on_hover_fn: None,
461 on_press_fn: None,
462 on_release_fn: None,
463 on_focus_fn: None,
464 on_unfocus_fn: None,
465 text_input_on_changed_fn: None,
466 text_input_on_submit_fn: None,
467 }
468 }
469
470 pub fn text(&mut self, text: &str, config_fn: impl FnOnce(&mut TextConfig) -> &mut TextConfig) {
472 let mut config = TextConfig::new();
473 config_fn(&mut config);
474 let text_config_index = self.ply.context.store_text_element_config(config);
475 self.ply.context.open_text_element(text, text_config_index);
476 }
477
478 pub fn scroll_offset(&self) -> Vector2 {
480 self.ply.context.get_scroll_offset()
481 }
482
483 pub fn hovered(&self) -> bool {
485 self.ply.context.hovered()
486 }
487
488 pub fn pressed(&self) -> bool {
491 self.ply.context.pressed()
492 }
493
494 pub fn focused(&self) -> bool {
496 self.ply.context.focused()
497 }
498}
499
500impl<CustomElementData: Clone + Default + std::fmt::Debug> Ply<CustomElementData> {
501 pub fn begin(
503 &mut self,
504 ) -> Ui<'_, CustomElementData> {
505 if !self.headless {
506 self.context.set_layout_dimensions(Dimensions::new(
507 macroquad::prelude::screen_width(),
508 macroquad::prelude::screen_height(),
509 ));
510
511 self.context.current_time = macroquad::prelude::get_time();
513 self.context.frame_delta_time = macroquad::prelude::get_frame_time();
514 }
515
516 self.context.update_text_input_blink_timers();
518
519 if !self.headless {
521 let (mx, my) = macroquad::prelude::mouse_position();
522 let is_down = macroquad::prelude::is_mouse_button_down(
523 macroquad::prelude::MouseButton::Left,
524 );
525
526 self.context.set_pointer_state(Vector2::new(mx, my), is_down);
529
530 {
531 use macroquad::prelude::{is_key_down, KeyCode};
532 let shift = is_key_down(KeyCode::LeftShift) || is_key_down(KeyCode::RightShift);
533 if shift {
534 if let Some(ref mut pending) = self.context.pending_text_click {
536 pending.3 = true;
537 }
538 }
539 }
540
541 let (scroll_x, scroll_y) = macroquad::prelude::mouse_wheel();
542 #[cfg(target_arch = "wasm32")]
543 const SCROLL_SPEED: f32 = 1.0;
544 #[cfg(not(target_arch = "wasm32"))]
545 const SCROLL_SPEED: f32 = 20.0;
546 let scroll_shift = {
548 use macroquad::prelude::{is_key_down, KeyCode};
549 is_key_down(KeyCode::LeftShift) || is_key_down(KeyCode::RightShift)
550 };
551 let scroll_delta = if scroll_shift {
552 Vector2::new(
554 (scroll_x + scroll_y) * SCROLL_SPEED,
555 0.0,
556 )
557 } else {
558 Vector2::new(scroll_x * SCROLL_SPEED, scroll_y * SCROLL_SPEED)
559 };
560
561 let text_consumed_scroll = self.context.update_text_input_pointer_scroll(scroll_delta);
563 self.context.clamp_text_input_scroll();
564
565 let container_scroll = if text_consumed_scroll {
567 Vector2::new(0.0, 0.0)
568 } else {
569 scroll_delta
570 };
571 self.context.update_scroll_containers(
572 true,
573 container_scroll,
574 macroquad::prelude::get_frame_time(),
575 );
576
577 use macroquad::prelude::{is_key_pressed, is_key_down, is_key_released, KeyCode};
579
580 let text_input_focused = self.context.is_text_input_focused();
581 let current_focused_id = self.context.focused_element_id;
582
583 if current_focused_id != self.text_input_repeat_focus_id {
586 self.text_input_repeat_key = 0;
587 self.text_input_repeat_focus_id = current_focused_id;
588 }
589
590 if is_key_pressed(KeyCode::Tab) {
592 let shift = is_key_down(KeyCode::LeftShift) || is_key_down(KeyCode::RightShift);
593 self.context.cycle_focus(shift);
594 } else if text_input_focused {
595 let shift = is_key_down(KeyCode::LeftShift) || is_key_down(KeyCode::RightShift);
597 let ctrl = is_key_down(KeyCode::LeftControl) || is_key_down(KeyCode::RightControl);
598 let time = self.context.current_time;
599
600 const INITIAL_DELAY: f64 = 0.5;
602 const REPEAT_INTERVAL: f64 = 0.033;
603
604 macro_rules! key_fires {
606 ($key:expr, $id:expr) => {{
607 if is_key_pressed($key) {
608 self.text_input_repeat_key = $id;
609 self.text_input_repeat_first = time;
610 self.text_input_repeat_last = time;
611 true
612 } else if is_key_down($key) && self.text_input_repeat_key == $id {
613 let since_first = time - self.text_input_repeat_first;
614 let since_last = time - self.text_input_repeat_last;
615 if since_first > INITIAL_DELAY && since_last > REPEAT_INTERVAL {
616 self.text_input_repeat_last = time;
617 true
618 } else {
619 false
620 }
621 } else {
622 false
623 }
624 }};
625 }
626
627 let mut cursor_moved = false;
629 if key_fires!(KeyCode::Left, 1) {
630 if ctrl {
631 self.context.process_text_input_action(engine::TextInputAction::MoveWordLeft { shift });
632 } else {
633 self.context.process_text_input_action(engine::TextInputAction::MoveLeft { shift });
634 }
635 cursor_moved = true;
636 }
637 if key_fires!(KeyCode::Right, 2) {
638 if ctrl {
639 self.context.process_text_input_action(engine::TextInputAction::MoveWordRight { shift });
640 } else {
641 self.context.process_text_input_action(engine::TextInputAction::MoveRight { shift });
642 }
643 cursor_moved = true;
644 }
645 if key_fires!(KeyCode::Backspace, 3) {
646 if ctrl {
647 self.context.process_text_input_action(engine::TextInputAction::BackspaceWord);
648 } else {
649 self.context.process_text_input_action(engine::TextInputAction::Backspace);
650 }
651 cursor_moved = true;
652 }
653 if key_fires!(KeyCode::Delete, 4) {
654 if ctrl {
655 self.context.process_text_input_action(engine::TextInputAction::DeleteWord);
656 } else {
657 self.context.process_text_input_action(engine::TextInputAction::Delete);
658 }
659 cursor_moved = true;
660 }
661 if key_fires!(KeyCode::Home, 5) {
662 self.context.process_text_input_action(engine::TextInputAction::MoveHome { shift });
663 cursor_moved = true;
664 }
665 if key_fires!(KeyCode::End, 6) {
666 self.context.process_text_input_action(engine::TextInputAction::MoveEnd { shift });
667 cursor_moved = true;
668 }
669
670 if self.context.is_focused_text_input_multiline() {
672 if key_fires!(KeyCode::Up, 7) {
673 self.context.process_text_input_action(engine::TextInputAction::MoveUp { shift });
674 cursor_moved = true;
675 }
676 if key_fires!(KeyCode::Down, 8) {
677 self.context.process_text_input_action(engine::TextInputAction::MoveDown { shift });
678 cursor_moved = true;
679 }
680 }
681
682 if is_key_pressed(KeyCode::Enter) {
684 self.context.process_text_input_action(engine::TextInputAction::Submit);
685 cursor_moved = true;
686 }
687 if ctrl && is_key_pressed(KeyCode::A) {
688 self.context.process_text_input_action(engine::TextInputAction::SelectAll);
689 }
690 if ctrl && is_key_pressed(KeyCode::Z) {
691 if shift {
692 self.context.process_text_input_action(engine::TextInputAction::Redo);
693 } else {
694 self.context.process_text_input_action(engine::TextInputAction::Undo);
695 }
696 cursor_moved = true;
697 }
698 if ctrl && is_key_pressed(KeyCode::Y) {
699 self.context.process_text_input_action(engine::TextInputAction::Redo);
700 cursor_moved = true;
701 }
702 if ctrl && is_key_pressed(KeyCode::C) {
703 let elem_id = self.context.focused_element_id;
705 if let Some(state) = self.context.text_edit_states.get(&elem_id) {
706 #[cfg(feature = "text-styling")]
707 let selected = state.selected_text_styled();
708 #[cfg(not(feature = "text-styling"))]
709 let selected = state.selected_text().to_string();
710 if !selected.is_empty() {
711 macroquad::miniquad::window::clipboard_set(&selected);
712 }
713 }
714 }
715 if ctrl && is_key_pressed(KeyCode::X) {
716 let elem_id = self.context.focused_element_id;
718 if let Some(state) = self.context.text_edit_states.get(&elem_id) {
719 #[cfg(feature = "text-styling")]
720 let selected = state.selected_text_styled();
721 #[cfg(not(feature = "text-styling"))]
722 let selected = state.selected_text().to_string();
723 if !selected.is_empty() {
724 macroquad::miniquad::window::clipboard_set(&selected);
725 }
726 }
727 self.context.process_text_input_action(engine::TextInputAction::Cut);
728 cursor_moved = true;
729 }
730 if ctrl && is_key_pressed(KeyCode::V) {
731 if let Some(text) = macroquad::miniquad::window::clipboard_get() {
733 self.context.process_text_input_action(engine::TextInputAction::Paste { text });
734 cursor_moved = true;
735 }
736 }
737
738 if is_key_pressed(KeyCode::Escape) {
740 self.context.clear_focus();
741 }
742
743 if self.text_input_repeat_key != 0 {
745 let still_down = match self.text_input_repeat_key {
746 1 => is_key_down(KeyCode::Left),
747 2 => is_key_down(KeyCode::Right),
748 3 => is_key_down(KeyCode::Backspace),
749 4 => is_key_down(KeyCode::Delete),
750 5 => is_key_down(KeyCode::Home),
751 6 => is_key_down(KeyCode::End),
752 7 => is_key_down(KeyCode::Up),
753 8 => is_key_down(KeyCode::Down),
754 _ => false,
755 };
756 if !still_down {
757 self.text_input_repeat_key = 0;
758 }
759 }
760
761 while let Some(ch) = macroquad::prelude::get_char_pressed() {
763 if !ch.is_control() && !ctrl {
765 self.context.process_text_input_char(ch);
766 cursor_moved = true;
767 }
768 }
769
770 if cursor_moved {
773 self.context.update_text_input_scroll();
774 }
775 self.context.clamp_text_input_scroll();
776 } else {
777 if is_key_pressed(KeyCode::Right) { self.context.arrow_focus(engine::ArrowDirection::Right); }
779 if is_key_pressed(KeyCode::Left) { self.context.arrow_focus(engine::ArrowDirection::Left); }
780 if is_key_pressed(KeyCode::Up) { self.context.arrow_focus(engine::ArrowDirection::Up); }
781 if is_key_pressed(KeyCode::Down) { self.context.arrow_focus(engine::ArrowDirection::Down); }
782
783 let activate_pressed = is_key_pressed(KeyCode::Enter) || is_key_pressed(KeyCode::Space);
784 let activate_released = is_key_released(KeyCode::Enter) || is_key_released(KeyCode::Space);
785 self.context.handle_keyboard_activation(activate_pressed, activate_released);
786 }
787 }
788
789 {
791 let text_input_focused = self.context.is_text_input_focused();
792 if text_input_focused != self.was_text_input_focused {
793 #[cfg(not(any(target_arch = "wasm32", target_os = "linux")))]
794 {
795 macroquad::miniquad::window::show_keyboard(text_input_focused);
796 }
797 #[cfg(target_arch = "wasm32")]
798 {
799 unsafe { ply_show_virtual_keyboard(text_input_focused); }
800 }
801 self.was_text_input_focused = text_input_focused;
802 }
803 }
804
805 self.context.begin_layout();
806 Ui {
807 ply: self,
808 }
809 }
810
811 pub async fn new(default_font: &'static renderer::FontAsset) -> Self {
813 renderer::FontManager::load_default(default_font).await;
814
815 let dimensions = Dimensions::new(
816 macroquad::prelude::screen_width(),
817 macroquad::prelude::screen_height(),
818 );
819 let mut ply = Self {
820 context: engine::PlyContext::new(dimensions),
821 headless: false,
822 text_input_repeat_key: 0,
823 text_input_repeat_first: 0.0,
824 text_input_repeat_last: 0.0,
825 text_input_repeat_focus_id: 0,
826 was_text_input_focused: false,
827 #[cfg(all(feature = "a11y", target_arch = "wasm32"))]
828 web_a11y_state: accessibility_web::WebAccessibilityState::default(),
829 #[cfg(all(feature = "a11y", not(target_arch = "wasm32")))]
830 native_a11y_state: accessibility_native::NativeAccessibilityState::default(),
831 };
832 ply.context.default_font_key = default_font.key();
833 ply.set_measure_text_function(renderer::create_measure_text_function());
834 ply
835 }
836
837 pub fn new_headless(dimensions: Dimensions) -> Self {
842 Self {
843 context: engine::PlyContext::new(dimensions),
844 headless: true,
845 text_input_repeat_key: 0,
846 text_input_repeat_first: 0.0,
847 text_input_repeat_last: 0.0,
848 text_input_repeat_focus_id: 0,
849 was_text_input_focused: false,
850 #[cfg(all(feature = "a11y", target_arch = "wasm32"))]
851 web_a11y_state: accessibility_web::WebAccessibilityState::default(),
852 #[cfg(all(feature = "a11y", not(target_arch = "wasm32")))]
853 native_a11y_state: accessibility_native::NativeAccessibilityState::default(),
854 }
855 }
856
857 pub fn pointer_over(&self, cfg: impl Into<Id>) -> bool {
859 self.context.pointer_over(cfg.into())
860 }
861
862 pub fn pointer_over_ids(&self) -> Vec<Id> {
864 self.context.get_pointer_over_ids().to_vec()
865 }
866
867 pub fn set_measure_text_function<F>(&mut self, callback: F)
869 where
870 F: Fn(&str, &TextConfig) -> Dimensions + 'static,
871 {
872 self.context.set_measure_text_function(Box::new(
873 move |text: &str, config: &TextConfig| -> Dimensions {
874 callback(text, config)
875 },
876 ));
877 }
878
879 pub fn max_element_count(&mut self, max_element_count: u32) {
882 self.context.set_max_element_count(max_element_count as i32);
883 }
884
885 pub fn max_measure_text_cache_word_count(&mut self, count: u32) {
888 self.context.set_max_measure_text_cache_word_count(count as i32);
889 }
890
891 pub fn set_debug_mode(&mut self, enable: bool) {
893 self.context.set_debug_mode_enabled(enable);
894 }
895
896 pub fn is_debug_mode(&self) -> bool {
898 self.context.is_debug_mode_enabled()
899 }
900
901 pub fn set_culling(&mut self, enable: bool) {
903 self.context.set_culling_enabled(enable);
904 }
905
906 pub fn set_layout_dimensions(&mut self, dimensions: Dimensions) {
909 self.context.set_layout_dimensions(dimensions);
910 }
911
912 pub fn pointer_state(&mut self, position: Vector2, is_down: bool) {
915 self.context.set_pointer_state(position, is_down);
916 }
917
918 pub fn update_scroll_containers(
920 &mut self,
921 drag_scrolling_enabled: bool,
922 scroll_delta: Vector2,
923 delta_time: f32,
924 ) {
925 self.context
926 .update_scroll_containers(drag_scrolling_enabled, scroll_delta, delta_time);
927 }
928
929 pub fn focused_element(&self) -> Option<Id> {
931 self.context.focused_element()
932 }
933
934 pub fn set_focus(&mut self, id: impl Into<Id>) {
936 self.context.set_focus(id.into().id);
937 }
938
939 pub fn clear_focus(&mut self) {
941 self.context.clear_focus();
942 }
943
944 pub fn get_text_value(&self, id: impl Into<Id>) -> &str {
947 self.context.get_text_value(id.into().id)
948 }
949
950 pub fn set_text_value(&mut self, id: impl Into<Id>, value: &str) {
952 self.context.set_text_value(id.into().id, value);
953 }
954
955 pub fn get_cursor_pos(&self, id: impl Into<Id>) -> usize {
958 self.context.get_cursor_pos(id.into().id)
959 }
960
961 pub fn set_cursor_pos(&mut self, id: impl Into<Id>, pos: usize) {
964 self.context.set_cursor_pos(id.into().id, pos);
965 }
966
967 pub fn get_selection_range(&self, id: impl Into<Id>) -> Option<(usize, usize)> {
969 self.context.get_selection_range(id.into().id)
970 }
971
972 pub fn set_selection(&mut self, id: impl Into<Id>, anchor: usize, cursor: usize) {
975 self.context.set_selection(id.into().id, anchor, cursor);
976 }
977
978 pub fn is_pressed(&self, id: impl Into<Id>) -> bool {
980 self.context.is_element_pressed(id.into().id)
981 }
982
983 pub fn bounding_box(&self, id: impl Into<Id>) -> Option<math::BoundingBox> {
985 self.context.get_element_data(id.into())
986 }
987
988 pub fn scroll_container_data(&self, id: impl Into<Id>) -> Option<engine::ScrollContainerData> {
990 let data = self.context.get_scroll_container_data(id.into());
991 if data.found {
992 Some(data)
993 } else {
994 None
995 }
996 }
997
998 pub fn eval(&mut self) -> Vec<RenderCommand<CustomElementData>> {
1000 #[cfg(feature = "net")]
1002 net::NET_MANAGER.lock().unwrap().clean();
1003
1004 let commands = self.context.end_layout();
1005 let mut result = Vec::new();
1006 for cmd in commands {
1007 result.push(RenderCommand::from_engine_render_command(cmd));
1008 }
1009
1010 #[cfg(all(feature = "a11y", target_arch = "wasm32"))]
1012 {
1013 accessibility_web::sync_accessibility_tree(
1014 &mut self.web_a11y_state,
1015 &self.context.accessibility_configs,
1016 &self.context.accessibility_element_order,
1017 self.context.focused_element_id,
1018 );
1019 }
1020
1021 #[cfg(all(feature = "a11y", not(target_arch = "wasm32")))]
1023 {
1024 let a11y_actions = accessibility_native::sync_accessibility_tree(
1025 &mut self.native_a11y_state,
1026 &self.context.accessibility_configs,
1027 &self.context.accessibility_element_order,
1028 self.context.focused_element_id,
1029 );
1030 for action in a11y_actions {
1031 match action {
1032 accessibility_native::PendingA11yAction::Focus(target_id) => {
1033 self.context.change_focus(target_id);
1034 }
1035 accessibility_native::PendingA11yAction::Click(target_id) => {
1036 self.context.fire_press(target_id);
1037 }
1038 }
1039 }
1040 }
1041
1042 result
1043 }
1044
1045 pub async fn show(
1047 &mut self,
1048 handle_custom_command: impl Fn(&RenderCommand<CustomElementData>),
1049 ) {
1050 let commands = self.eval();
1051 renderer::render(commands, handle_custom_command).await;
1052 }
1053}
1054
1055#[cfg(target_arch = "wasm32")]
1056extern "C" {
1057 fn ply_show_virtual_keyboard(show: bool);
1058}
1059
1060#[cfg(test)]
1061mod tests {
1062 use super::*;
1063 use color::Color;
1064 use layout::{Padding, Sizing};
1065
1066 #[rustfmt::skip]
1067 #[test]
1068 fn test_begin() {
1069 let mut ply = Ply::<()>::new_headless(Dimensions::new(800.0, 600.0));
1070
1071 ply.set_measure_text_function(|_, _| {
1072 Dimensions::new(100.0, 24.0)
1073 });
1074
1075 let mut ui = ply.begin();
1076
1077 ui.element().width(fixed!(100.0)).height(fixed!(100.0))
1078 .background_color(0xFFFFFF)
1079 .children(|ui| {
1080 ui.element().width(fixed!(100.0)).height(fixed!(100.0))
1081 .background_color(0xFFFFFF)
1082 .children(|ui| {
1083 ui.element().width(fixed!(100.0)).height(fixed!(100.0))
1084 .background_color(0xFFFFFF)
1085 .children(|ui| {
1086 ui.text("test", |t| t
1087 .color(0xFFFFFF)
1088 .font_size(24)
1089 );
1090 });
1091 });
1092 });
1093
1094 ui.element()
1095 .border(|b| b
1096 .color(0xFFFF00)
1097 .all(2)
1098 )
1099 .corner_radius(10.0)
1100 .children(|ui| {
1101 ui.element().width(fixed!(50.0)).height(fixed!(50.0))
1102 .background_color(0x00FFFF)
1103 .empty();
1104 });
1105
1106 let items = ui.eval();
1107
1108 for item in &items {
1109 println!(
1110 "id: {}\nbbox: {:?}\nconfig: {:?}",
1111 item.id, item.bounding_box, item.config,
1112 );
1113 }
1114
1115 assert_eq!(items.len(), 6);
1116
1117 assert_eq!(items[0].bounding_box.x, 0.0);
1118 assert_eq!(items[0].bounding_box.y, 0.0);
1119 assert_eq!(items[0].bounding_box.width, 100.0);
1120 assert_eq!(items[0].bounding_box.height, 100.0);
1121 match &items[0].config {
1122 render_commands::RenderCommandConfig::Rectangle(rect) => {
1123 assert_eq!(rect.color.r, 255.0);
1124 assert_eq!(rect.color.g, 255.0);
1125 assert_eq!(rect.color.b, 255.0);
1126 assert_eq!(rect.color.a, 255.0);
1127 }
1128 _ => panic!("Expected Rectangle config for item 0"),
1129 }
1130
1131 assert_eq!(items[1].bounding_box.x, 0.0);
1132 assert_eq!(items[1].bounding_box.y, 0.0);
1133 assert_eq!(items[1].bounding_box.width, 100.0);
1134 assert_eq!(items[1].bounding_box.height, 100.0);
1135 match &items[1].config {
1136 render_commands::RenderCommandConfig::Rectangle(rect) => {
1137 assert_eq!(rect.color.r, 255.0);
1138 assert_eq!(rect.color.g, 255.0);
1139 assert_eq!(rect.color.b, 255.0);
1140 assert_eq!(rect.color.a, 255.0);
1141 }
1142 _ => panic!("Expected Rectangle config for item 1"),
1143 }
1144
1145 assert_eq!(items[2].bounding_box.x, 0.0);
1146 assert_eq!(items[2].bounding_box.y, 0.0);
1147 assert_eq!(items[2].bounding_box.width, 100.0);
1148 assert_eq!(items[2].bounding_box.height, 100.0);
1149 match &items[2].config {
1150 render_commands::RenderCommandConfig::Rectangle(rect) => {
1151 assert_eq!(rect.color.r, 255.0);
1152 assert_eq!(rect.color.g, 255.0);
1153 assert_eq!(rect.color.b, 255.0);
1154 assert_eq!(rect.color.a, 255.0);
1155 }
1156 _ => panic!("Expected Rectangle config for item 2"),
1157 }
1158
1159 assert_eq!(items[3].bounding_box.x, 0.0);
1160 assert_eq!(items[3].bounding_box.y, 0.0);
1161 assert_eq!(items[3].bounding_box.width, 100.0);
1162 assert_eq!(items[3].bounding_box.height, 24.0);
1163 match &items[3].config {
1164 render_commands::RenderCommandConfig::Text(text) => {
1165 assert_eq!(text.text, "test");
1166 assert_eq!(text.color.r, 255.0);
1167 assert_eq!(text.color.g, 255.0);
1168 assert_eq!(text.color.b, 255.0);
1169 assert_eq!(text.color.a, 255.0);
1170 assert_eq!(text.font_size, 24);
1171 }
1172 _ => panic!("Expected Text config for item 3"),
1173 }
1174
1175 assert_eq!(items[4].bounding_box.x, 100.0);
1176 assert_eq!(items[4].bounding_box.y, 0.0);
1177 assert_eq!(items[4].bounding_box.width, 50.0);
1178 assert_eq!(items[4].bounding_box.height, 50.0);
1179 match &items[4].config {
1180 render_commands::RenderCommandConfig::Rectangle(rect) => {
1181 assert_eq!(rect.color.r, 0.0);
1182 assert_eq!(rect.color.g, 255.0);
1183 assert_eq!(rect.color.b, 255.0);
1184 assert_eq!(rect.color.a, 255.0);
1185 }
1186 _ => panic!("Expected Rectangle config for item 4"),
1187 }
1188
1189 assert_eq!(items[5].bounding_box.x, 100.0);
1190 assert_eq!(items[5].bounding_box.y, 0.0);
1191 assert_eq!(items[5].bounding_box.width, 50.0);
1192 assert_eq!(items[5].bounding_box.height, 50.0);
1193 match &items[5].config {
1194 render_commands::RenderCommandConfig::Border(border) => {
1195 assert_eq!(border.color.r, 255.0);
1196 assert_eq!(border.color.g, 255.0);
1197 assert_eq!(border.color.b, 0.0);
1198 assert_eq!(border.color.a, 255.0);
1199 assert_eq!(border.corner_radii.top_left, 10.0);
1200 assert_eq!(border.corner_radii.top_right, 10.0);
1201 assert_eq!(border.corner_radii.bottom_left, 10.0);
1202 assert_eq!(border.corner_radii.bottom_right, 10.0);
1203 assert_eq!(border.width.left, 2);
1204 assert_eq!(border.width.right, 2);
1205 assert_eq!(border.width.top, 2);
1206 assert_eq!(border.width.bottom, 2);
1207 }
1208 _ => panic!("Expected Border config for item 5"),
1209 }
1210 }
1211
1212 #[rustfmt::skip]
1213 #[test]
1214 fn test_example() {
1215 let mut ply = Ply::<()>::new_headless(Dimensions::new(1000.0, 1000.0));
1216
1217 let mut ui = ply.begin();
1218
1219 ui.set_measure_text_function(|_, _| {
1220 Dimensions::new(100.0, 24.0)
1221 });
1222
1223 for &(label, level) in &[("Road", 1), ("Wall", 2), ("Tower", 3)] {
1224 ui.element().width(grow!()).height(fixed!(36.0))
1225 .layout(|l| l
1226 .direction(crate::layout::LayoutDirection::LeftToRight)
1227 .gap(12)
1228 .align(crate::align::AlignX::Left, crate::align::AlignY::CenterY)
1229 )
1230 .children(|ui| {
1231 ui.text(label, |t| t
1232 .font_size(18)
1233 .color(0xFFFFFF)
1234 );
1235 ui.element().width(grow!()).height(fixed!(18.0))
1236 .corner_radius(9.0)
1237 .background_color(0x555555)
1238 .children(|ui| {
1239 ui.element()
1240 .width(fixed!(300.0 * level as f32 / 3.0))
1241 .height(grow!())
1242 .corner_radius(9.0)
1243 .background_color(0x45A85A)
1244 .empty();
1245 });
1246 });
1247 }
1248
1249 let items = ui.eval();
1250
1251 for item in &items {
1252 println!(
1253 "id: {}\nbbox: {:?}\nconfig: {:?}",
1254 item.id, item.bounding_box, item.config,
1255 );
1256 }
1257
1258 assert_eq!(items.len(), 9);
1259
1260 assert_eq!(items[0].bounding_box.x, 0.0);
1262 assert_eq!(items[0].bounding_box.y, 6.0);
1263 assert_eq!(items[0].bounding_box.width, 100.0);
1264 assert_eq!(items[0].bounding_box.height, 24.0);
1265 match &items[0].config {
1266 render_commands::RenderCommandConfig::Text(text) => {
1267 assert_eq!(text.text, "Road");
1268 assert_eq!(text.color.r, 255.0);
1269 assert_eq!(text.color.g, 255.0);
1270 assert_eq!(text.color.b, 255.0);
1271 assert_eq!(text.color.a, 255.0);
1272 assert_eq!(text.font_size, 18);
1273 }
1274 _ => panic!("Expected Text config for item 0"),
1275 }
1276
1277 assert_eq!(items[1].bounding_box.x, 112.0);
1279 assert_eq!(items[1].bounding_box.y, 9.0);
1280 assert_eq!(items[1].bounding_box.width, 163.99142);
1281 assert_eq!(items[1].bounding_box.height, 18.0);
1282 match &items[1].config {
1283 render_commands::RenderCommandConfig::Rectangle(rect) => {
1284 assert_eq!(rect.color.r, 85.0);
1285 assert_eq!(rect.color.g, 85.0);
1286 assert_eq!(rect.color.b, 85.0);
1287 assert_eq!(rect.color.a, 255.0);
1288 assert_eq!(rect.corner_radii.top_left, 9.0);
1289 assert_eq!(rect.corner_radii.top_right, 9.0);
1290 assert_eq!(rect.corner_radii.bottom_left, 9.0);
1291 assert_eq!(rect.corner_radii.bottom_right, 9.0);
1292 }
1293 _ => panic!("Expected Rectangle config for item 1"),
1294 }
1295
1296 assert_eq!(items[2].bounding_box.x, 112.0);
1298 assert_eq!(items[2].bounding_box.y, 9.0);
1299 assert_eq!(items[2].bounding_box.width, 100.0);
1300 assert_eq!(items[2].bounding_box.height, 18.0);
1301 match &items[2].config {
1302 render_commands::RenderCommandConfig::Rectangle(rect) => {
1303 assert_eq!(rect.color.r, 69.0);
1304 assert_eq!(rect.color.g, 168.0);
1305 assert_eq!(rect.color.b, 90.0);
1306 assert_eq!(rect.color.a, 255.0);
1307 assert_eq!(rect.corner_radii.top_left, 9.0);
1308 assert_eq!(rect.corner_radii.top_right, 9.0);
1309 assert_eq!(rect.corner_radii.bottom_left, 9.0);
1310 assert_eq!(rect.corner_radii.bottom_right, 9.0);
1311 }
1312 _ => panic!("Expected Rectangle config for item 2"),
1313 }
1314
1315 assert_eq!(items[3].bounding_box.x, 275.99142);
1317 assert_eq!(items[3].bounding_box.y, 6.0);
1318 assert_eq!(items[3].bounding_box.width, 100.0);
1319 assert_eq!(items[3].bounding_box.height, 24.0);
1320 match &items[3].config {
1321 render_commands::RenderCommandConfig::Text(text) => {
1322 assert_eq!(text.text, "Wall");
1323 assert_eq!(text.color.r, 255.0);
1324 assert_eq!(text.color.g, 255.0);
1325 assert_eq!(text.color.b, 255.0);
1326 assert_eq!(text.color.a, 255.0);
1327 assert_eq!(text.font_size, 18);
1328 }
1329 _ => panic!("Expected Text config for item 3"),
1330 }
1331
1332 assert_eq!(items[4].bounding_box.x, 387.99142);
1334 assert_eq!(items[4].bounding_box.y, 9.0);
1335 assert_eq!(items[4].bounding_box.width, 200.0);
1336 assert_eq!(items[4].bounding_box.height, 18.0);
1337 match &items[4].config {
1338 render_commands::RenderCommandConfig::Rectangle(rect) => {
1339 assert_eq!(rect.color.r, 85.0);
1340 assert_eq!(rect.color.g, 85.0);
1341 assert_eq!(rect.color.b, 85.0);
1342 assert_eq!(rect.color.a, 255.0);
1343 assert_eq!(rect.corner_radii.top_left, 9.0);
1344 assert_eq!(rect.corner_radii.top_right, 9.0);
1345 assert_eq!(rect.corner_radii.bottom_left, 9.0);
1346 assert_eq!(rect.corner_radii.bottom_right, 9.0);
1347 }
1348 _ => panic!("Expected Rectangle config for item 4"),
1349 }
1350
1351 assert_eq!(items[5].bounding_box.x, 387.99142);
1353 assert_eq!(items[5].bounding_box.y, 9.0);
1354 assert_eq!(items[5].bounding_box.width, 200.0);
1355 assert_eq!(items[5].bounding_box.height, 18.0);
1356 match &items[5].config {
1357 render_commands::RenderCommandConfig::Rectangle(rect) => {
1358 assert_eq!(rect.color.r, 69.0);
1359 assert_eq!(rect.color.g, 168.0);
1360 assert_eq!(rect.color.b, 90.0);
1361 assert_eq!(rect.color.a, 255.0);
1362 assert_eq!(rect.corner_radii.top_left, 9.0);
1363 assert_eq!(rect.corner_radii.top_right, 9.0);
1364 assert_eq!(rect.corner_radii.bottom_left, 9.0);
1365 assert_eq!(rect.corner_radii.bottom_right, 9.0);
1366 }
1367 _ => panic!("Expected Rectangle config for item 5"),
1368 }
1369
1370 assert_eq!(items[6].bounding_box.x, 587.99146);
1372 assert_eq!(items[6].bounding_box.y, 6.0);
1373 assert_eq!(items[6].bounding_box.width, 100.0);
1374 assert_eq!(items[6].bounding_box.height, 24.0);
1375 match &items[6].config {
1376 render_commands::RenderCommandConfig::Text(text) => {
1377 assert_eq!(text.text, "Tower");
1378 assert_eq!(text.color.r, 255.0);
1379 assert_eq!(text.color.g, 255.0);
1380 assert_eq!(text.color.b, 255.0);
1381 assert_eq!(text.color.a, 255.0);
1382 assert_eq!(text.font_size, 18);
1383 }
1384 _ => panic!("Expected Text config for item 6"),
1385 }
1386
1387 assert_eq!(items[7].bounding_box.x, 699.99146);
1389 assert_eq!(items[7].bounding_box.y, 9.0);
1390 assert_eq!(items[7].bounding_box.width, 300.0);
1391 assert_eq!(items[7].bounding_box.height, 18.0);
1392 match &items[7].config {
1393 render_commands::RenderCommandConfig::Rectangle(rect) => {
1394 assert_eq!(rect.color.r, 85.0);
1395 assert_eq!(rect.color.g, 85.0);
1396 assert_eq!(rect.color.b, 85.0);
1397 assert_eq!(rect.color.a, 255.0);
1398 assert_eq!(rect.corner_radii.top_left, 9.0);
1399 assert_eq!(rect.corner_radii.top_right, 9.0);
1400 assert_eq!(rect.corner_radii.bottom_left, 9.0);
1401 assert_eq!(rect.corner_radii.bottom_right, 9.0);
1402 }
1403 _ => panic!("Expected Rectangle config for item 7"),
1404 }
1405
1406 assert_eq!(items[8].bounding_box.x, 699.99146);
1408 assert_eq!(items[8].bounding_box.y, 9.0);
1409 assert_eq!(items[8].bounding_box.width, 300.0);
1410 assert_eq!(items[8].bounding_box.height, 18.0);
1411 match &items[8].config {
1412 render_commands::RenderCommandConfig::Rectangle(rect) => {
1413 assert_eq!(rect.color.r, 69.0);
1414 assert_eq!(rect.color.g, 168.0);
1415 assert_eq!(rect.color.b, 90.0);
1416 assert_eq!(rect.color.a, 255.0);
1417 assert_eq!(rect.corner_radii.top_left, 9.0);
1418 assert_eq!(rect.corner_radii.top_right, 9.0);
1419 assert_eq!(rect.corner_radii.bottom_left, 9.0);
1420 assert_eq!(rect.corner_radii.bottom_right, 9.0);
1421 }
1422 _ => panic!("Expected Rectangle config for item 8"),
1423 }
1424 }
1425
1426 #[rustfmt::skip]
1427 #[test]
1428 fn test_floating() {
1429 let mut ply = Ply::<()>::new_headless(Dimensions::new(1000.0, 1000.0));
1430
1431 let mut ui = ply.begin();
1432
1433 ui.set_measure_text_function(|_, _| {
1434 Dimensions::new(100.0, 24.0)
1435 });
1436
1437 ui.element().width(fixed!(20.0)).height(fixed!(20.0))
1438 .layout(|l| l.align(crate::align::AlignX::CenterX, crate::align::AlignY::CenterY))
1439 .floating(|f| f
1440 .attach_root()
1441 .anchor((crate::align::AlignX::CenterX, crate::align::AlignY::CenterY), (crate::align::AlignX::Left, crate::align::AlignY::Top))
1442 .offset(100.0, 150.0)
1443 .passthrough()
1444 .z_index(110)
1445 )
1446 .corner_radius(10.0)
1447 .background_color(0x4488DD)
1448 .children(|ui| {
1449 ui.text("Re", |t| t
1450 .font_size(6)
1451 .color(0xFFFFFF)
1452 );
1453 });
1454
1455 let items = ui.eval();
1456
1457 for item in &items {
1458 println!(
1459 "id: {}\nbbox: {:?}\nconfig: {:?}",
1460 item.id, item.bounding_box, item.config,
1461 );
1462 }
1463
1464 assert_eq!(items.len(), 2);
1465
1466 assert_eq!(items[0].bounding_box.x, 90.0);
1467 assert_eq!(items[0].bounding_box.y, 140.0);
1468 assert_eq!(items[0].bounding_box.width, 20.0);
1469 assert_eq!(items[0].bounding_box.height, 20.0);
1470 match &items[0].config {
1471 render_commands::RenderCommandConfig::Rectangle(rect) => {
1472 assert_eq!(rect.color.r, 68.0);
1473 assert_eq!(rect.color.g, 136.0);
1474 assert_eq!(rect.color.b, 221.0);
1475 assert_eq!(rect.color.a, 255.0);
1476 assert_eq!(rect.corner_radii.top_left, 10.0);
1477 assert_eq!(rect.corner_radii.top_right, 10.0);
1478 assert_eq!(rect.corner_radii.bottom_left, 10.0);
1479 assert_eq!(rect.corner_radii.bottom_right, 10.0);
1480 }
1481 _ => panic!("Expected Rectangle config for item 0"),
1482 }
1483
1484 assert_eq!(items[1].bounding_box.x, 50.0);
1485 assert_eq!(items[1].bounding_box.y, 138.0);
1486 assert_eq!(items[1].bounding_box.width, 100.0);
1487 assert_eq!(items[1].bounding_box.height, 24.0);
1488 match &items[1].config {
1489 render_commands::RenderCommandConfig::Text(text) => {
1490 assert_eq!(text.text, "Re");
1491 assert_eq!(text.color.r, 255.0);
1492 assert_eq!(text.color.g, 255.0);
1493 assert_eq!(text.color.b, 255.0);
1494 assert_eq!(text.color.a, 255.0);
1495 assert_eq!(text.font_size, 6);
1496 }
1497 _ => panic!("Expected Text config for item 1"),
1498 }
1499 }
1500
1501 #[rustfmt::skip]
1502 #[test]
1503 fn test_simple_text_measure() {
1504 let mut ply = Ply::<()>::new_headless(Dimensions::new(800.0, 600.0));
1505
1506 ply.set_measure_text_function(|_text, _config| {
1507 Dimensions::default()
1508 });
1509
1510 let mut ui = ply.begin();
1511
1512 ui.element()
1513 .id("parent_rect")
1514 .width(Sizing::Fixed(100.0))
1515 .height(Sizing::Fixed(100.0))
1516 .layout(|l| l
1517 .padding(Padding::all(10))
1518 )
1519 .background_color(Color::rgb(255., 255., 255.))
1520 .children(|ui| {
1521 ui.text(&format!("{}", 1234), |t| t
1522 .color(Color::rgb(255., 255., 255.))
1523 .font_size(24)
1524 );
1525 });
1526
1527 let _items = ui.eval();
1528 }
1529
1530 #[rustfmt::skip]
1531 #[test]
1532 fn test_shader_begin_end() {
1533 use shaders::ShaderAsset;
1534
1535 let test_shader = ShaderAsset::Source {
1536 file_name: "test_effect.glsl",
1537 fragment: "#version 100\nprecision lowp float;\nvarying vec2 uv;\nuniform sampler2D Texture;\nvoid main() { gl_FragColor = texture2D(Texture, uv); }",
1538 };
1539
1540 let mut ply = Ply::<()>::new_headless(Dimensions::new(800.0, 600.0));
1541 ply.set_measure_text_function(|_, _| Dimensions::new(100.0, 24.0));
1542
1543 let mut ui = ply.begin();
1544
1545 ui.element()
1547 .width(fixed!(200.0)).height(fixed!(200.0))
1548 .background_color(0xFF0000)
1549 .shader(&test_shader, |s| {
1550 s.uniform("time", 1.0f32);
1551 })
1552 .children(|ui| {
1553 ui.element()
1554 .width(fixed!(100.0)).height(fixed!(100.0))
1555 .background_color(0x00FF00)
1556 .empty();
1557 });
1558
1559 let items = ui.eval();
1560
1561 for (i, item) in items.iter().enumerate() {
1562 println!(
1563 "[{}] config: {:?}, bbox: {:?}",
1564 i, item.config, item.bounding_box,
1565 );
1566 }
1567
1568 assert!(items.len() >= 4, "Expected at least 4 items, got {}", items.len());
1574
1575 match &items[0].config {
1576 render_commands::RenderCommandConfig::GroupBegin { shader, visual_rotation } => {
1577 let config = shader.as_ref().expect("GroupBegin should have shader config");
1578 assert!(!config.fragment.is_empty(), "GroupBegin should have fragment source");
1579 assert_eq!(config.uniforms.len(), 1);
1580 assert_eq!(config.uniforms[0].name, "time");
1581 assert!(visual_rotation.is_none(), "Shader-only group should have no visual_rotation");
1582 }
1583 other => panic!("Expected GroupBegin for item 0, got {:?}", other),
1584 }
1585
1586 match &items[1].config {
1587 render_commands::RenderCommandConfig::Rectangle(rect) => {
1588 assert_eq!(rect.color.r, 255.0);
1589 assert_eq!(rect.color.g, 0.0);
1590 assert_eq!(rect.color.b, 0.0);
1591 }
1592 other => panic!("Expected Rectangle for item 1, got {:?}", other),
1593 }
1594
1595 match &items[2].config {
1596 render_commands::RenderCommandConfig::Rectangle(rect) => {
1597 assert_eq!(rect.color.r, 0.0);
1598 assert_eq!(rect.color.g, 255.0);
1599 assert_eq!(rect.color.b, 0.0);
1600 }
1601 other => panic!("Expected Rectangle for item 2, got {:?}", other),
1602 }
1603
1604 match &items[3].config {
1605 render_commands::RenderCommandConfig::GroupEnd => {}
1606 other => panic!("Expected GroupEnd for item 3, got {:?}", other),
1607 }
1608 }
1609
1610 #[rustfmt::skip]
1611 #[test]
1612 fn test_multiple_shaders_nested() {
1613 use shaders::ShaderAsset;
1614
1615 let shader_a = ShaderAsset::Source {
1616 file_name: "shader_a.glsl",
1617 fragment: "#version 100\nprecision lowp float;\nvoid main() { gl_FragColor = vec4(1.0); }",
1618 };
1619 let shader_b = ShaderAsset::Source {
1620 file_name: "shader_b.glsl",
1621 fragment: "#version 100\nprecision lowp float;\nvoid main() { gl_FragColor = vec4(0.5); }",
1622 };
1623
1624 let mut ply = Ply::<()>::new_headless(Dimensions::new(800.0, 600.0));
1625 ply.set_measure_text_function(|_, _| Dimensions::new(100.0, 24.0));
1626
1627 let mut ui = ply.begin();
1628
1629 ui.element()
1631 .width(fixed!(200.0)).height(fixed!(200.0))
1632 .background_color(0xFFFFFF)
1633 .shader(&shader_a, |s| { s.uniform("val", 1.0f32); })
1634 .shader(&shader_b, |s| { s.uniform("val", 2.0f32); })
1635 .children(|ui| {
1636 ui.element()
1637 .width(fixed!(50.0)).height(fixed!(50.0))
1638 .background_color(0x0000FF)
1639 .empty();
1640 });
1641
1642 let items = ui.eval();
1643
1644 for (i, item) in items.iter().enumerate() {
1645 println!("[{}] config: {:?}", i, item.config);
1646 }
1647
1648 assert!(items.len() >= 6, "Expected at least 6 items, got {}", items.len());
1656
1657 match &items[0].config {
1658 render_commands::RenderCommandConfig::GroupBegin { shader, .. } => {
1659 let config = shader.as_ref().unwrap();
1660 assert!(config.fragment.contains("0.5"), "Expected shader_b fragment");
1662 }
1663 other => panic!("Expected GroupBegin(shader_b) for item 0, got {:?}", other),
1664 }
1665 match &items[1].config {
1666 render_commands::RenderCommandConfig::GroupBegin { shader, .. } => {
1667 let config = shader.as_ref().unwrap();
1668 assert!(config.fragment.contains("1.0"), "Expected shader_a fragment");
1670 }
1671 other => panic!("Expected GroupBegin(shader_a) for item 1, got {:?}", other),
1672 }
1673 match &items[2].config {
1674 render_commands::RenderCommandConfig::Rectangle(_) => {}
1675 other => panic!("Expected Rectangle for item 2, got {:?}", other),
1676 }
1677 match &items[3].config {
1678 render_commands::RenderCommandConfig::Rectangle(_) => {}
1679 other => panic!("Expected Rectangle for item 3, got {:?}", other),
1680 }
1681 match &items[4].config {
1682 render_commands::RenderCommandConfig::GroupEnd => {}
1683 other => panic!("Expected GroupEnd for item 4, got {:?}", other),
1684 }
1685 match &items[5].config {
1686 render_commands::RenderCommandConfig::GroupEnd => {}
1687 other => panic!("Expected GroupEnd for item 5, got {:?}", other),
1688 }
1689 }
1690
1691 #[rustfmt::skip]
1692 #[test]
1693 fn test_effect_on_render_command() {
1694 use shaders::ShaderAsset;
1695
1696 let effect_shader = ShaderAsset::Source {
1697 file_name: "gradient.glsl",
1698 fragment: "#version 100\nprecision lowp float;\nvoid main() { gl_FragColor = vec4(1.0); }",
1699 };
1700
1701 let mut ply = Ply::<()>::new_headless(Dimensions::new(800.0, 600.0));
1702
1703 let mut ui = ply.begin();
1704
1705 ui.element()
1706 .width(fixed!(200.0)).height(fixed!(100.0))
1707 .background_color(0xFF0000)
1708 .effect(&effect_shader, |s| {
1709 s.uniform("color_a", [1.0f32, 0.0, 0.0, 1.0])
1710 .uniform("color_b", [0.0f32, 0.0, 1.0, 1.0]);
1711 })
1712 .empty();
1713
1714 let items = ui.eval();
1715
1716 assert_eq!(items.len(), 1, "Expected 1 item, got {}", items.len());
1717 assert_eq!(items[0].effects.len(), 1, "Expected 1 effect");
1718 assert_eq!(items[0].effects[0].uniforms.len(), 2);
1719 assert_eq!(items[0].effects[0].uniforms[0].name, "color_a");
1720 assert_eq!(items[0].effects[0].uniforms[1].name, "color_b");
1721 }
1722
1723 #[rustfmt::skip]
1724 #[test]
1725 fn test_visual_rotation_emits_group() {
1726 let mut ply = Ply::<()>::new_headless(Dimensions::new(800.0, 600.0));
1727 let mut ui = ply.begin();
1728
1729 ui.element()
1730 .width(fixed!(100.0)).height(fixed!(50.0))
1731 .background_color(0xFF0000)
1732 .rotate_visual(|r| r.degrees(45.0))
1733 .empty();
1734
1735 let items = ui.eval();
1736
1737 assert_eq!(items.len(), 3, "Expected 3 items, got {}", items.len());
1739
1740 match &items[0].config {
1741 render_commands::RenderCommandConfig::GroupBegin { shader, visual_rotation } => {
1742 assert!(shader.is_none(), "Rotation-only group should have no shader");
1743 let vr = visual_rotation.as_ref().expect("Should have visual_rotation");
1744 assert!((vr.rotation_radians - 45.0_f32.to_radians()).abs() < 0.001);
1745 assert_eq!(vr.pivot_x, 0.5);
1746 assert_eq!(vr.pivot_y, 0.5);
1747 assert!(!vr.flip_x);
1748 assert!(!vr.flip_y);
1749 }
1750 other => panic!("Expected GroupBegin for item 0, got {:?}", other),
1751 }
1752
1753 match &items[1].config {
1754 render_commands::RenderCommandConfig::Rectangle(_) => {}
1755 other => panic!("Expected Rectangle for item 1, got {:?}", other),
1756 }
1757
1758 match &items[2].config {
1759 render_commands::RenderCommandConfig::GroupEnd => {}
1760 other => panic!("Expected GroupEnd for item 2, got {:?}", other),
1761 }
1762 }
1763
1764 #[rustfmt::skip]
1765 #[test]
1766 fn test_visual_rotation_with_shader_merged() {
1767 use shaders::ShaderAsset;
1768
1769 let test_shader = ShaderAsset::Source {
1770 file_name: "merge_test.glsl",
1771 fragment: "#version 100\nprecision lowp float;\nvoid main() { gl_FragColor = vec4(1.0); }",
1772 };
1773
1774 let mut ply = Ply::<()>::new_headless(Dimensions::new(800.0, 600.0));
1775 let mut ui = ply.begin();
1776
1777 ui.element()
1779 .width(fixed!(100.0)).height(fixed!(100.0))
1780 .background_color(0xFF0000)
1781 .shader(&test_shader, |s| { s.uniform("v", 1.0f32); })
1782 .rotate_visual(|r| r.degrees(30.0).pivot(0.0, 0.0))
1783 .empty();
1784
1785 let items = ui.eval();
1786
1787 assert_eq!(items.len(), 3, "Expected 3 items (merged), got {}", items.len());
1789
1790 match &items[0].config {
1791 render_commands::RenderCommandConfig::GroupBegin { shader, visual_rotation } => {
1792 assert!(shader.is_some(), "Merged group should have shader");
1793 let vr = visual_rotation.as_ref().expect("Merged group should have visual_rotation");
1794 assert!((vr.rotation_radians - 30.0_f32.to_radians()).abs() < 0.001);
1795 assert_eq!(vr.pivot_x, 0.0);
1796 assert_eq!(vr.pivot_y, 0.0);
1797 }
1798 other => panic!("Expected GroupBegin for item 0, got {:?}", other),
1799 }
1800 }
1801
1802 #[rustfmt::skip]
1803 #[test]
1804 fn test_visual_rotation_with_multiple_shaders() {
1805 use shaders::ShaderAsset;
1806
1807 let shader_a = ShaderAsset::Source {
1808 file_name: "vr_a.glsl",
1809 fragment: "#version 100\nprecision lowp float;\nvoid main() { gl_FragColor = vec4(1.0); }",
1810 };
1811 let shader_b = ShaderAsset::Source {
1812 file_name: "vr_b.glsl",
1813 fragment: "#version 100\nprecision lowp float;\nvoid main() { gl_FragColor = vec4(0.5); }",
1814 };
1815
1816 let mut ply = Ply::<()>::new_headless(Dimensions::new(800.0, 600.0));
1817 let mut ui = ply.begin();
1818
1819 ui.element()
1820 .width(fixed!(100.0)).height(fixed!(100.0))
1821 .background_color(0xFF0000)
1822 .shader(&shader_a, |s| { s.uniform("v", 1.0f32); })
1823 .shader(&shader_b, |s| { s.uniform("v", 2.0f32); })
1824 .rotate_visual(|r| r.degrees(90.0))
1825 .empty();
1826
1827 let items = ui.eval();
1828
1829 assert!(items.len() >= 5, "Expected at least 5 items, got {}", items.len());
1831
1832 match &items[0].config {
1834 render_commands::RenderCommandConfig::GroupBegin { shader, visual_rotation } => {
1835 assert!(shader.is_some(), "Outermost should have shader");
1836 assert!(visual_rotation.is_some(), "Outermost should have visual_rotation");
1837 }
1838 other => panic!("Expected GroupBegin for item 0, got {:?}", other),
1839 }
1840
1841 match &items[1].config {
1843 render_commands::RenderCommandConfig::GroupBegin { shader, visual_rotation } => {
1844 assert!(shader.is_some(), "Inner should have shader");
1845 assert!(visual_rotation.is_none(), "Inner should NOT have visual_rotation");
1846 }
1847 other => panic!("Expected GroupBegin for item 1, got {:?}", other),
1848 }
1849 }
1850
1851 #[rustfmt::skip]
1852 #[test]
1853 fn test_visual_rotation_noop_skipped() {
1854 let mut ply = Ply::<()>::new_headless(Dimensions::new(800.0, 600.0));
1855 let mut ui = ply.begin();
1856
1857 ui.element()
1859 .width(fixed!(100.0)).height(fixed!(100.0))
1860 .background_color(0xFF0000)
1861 .rotate_visual(|r| r.degrees(0.0))
1862 .empty();
1863
1864 let items = ui.eval();
1865
1866 assert_eq!(items.len(), 1, "Noop rotation should produce 1 item, got {}", items.len());
1868 match &items[0].config {
1869 render_commands::RenderCommandConfig::Rectangle(_) => {}
1870 other => panic!("Expected Rectangle, got {:?}", other),
1871 }
1872 }
1873
1874 #[rustfmt::skip]
1875 #[test]
1876 fn test_visual_rotation_flip_only() {
1877 let mut ply = Ply::<()>::new_headless(Dimensions::new(800.0, 600.0));
1878 let mut ui = ply.begin();
1879
1880 ui.element()
1882 .width(fixed!(100.0)).height(fixed!(100.0))
1883 .background_color(0xFF0000)
1884 .rotate_visual(|r| r.flip_x())
1885 .empty();
1886
1887 let items = ui.eval();
1888
1889 assert_eq!(items.len(), 3, "Flip-only should produce 3 items, got {}", items.len());
1891 match &items[0].config {
1892 render_commands::RenderCommandConfig::GroupBegin { visual_rotation, .. } => {
1893 let vr = visual_rotation.as_ref().expect("Should have rotation config");
1894 assert!(vr.flip_x);
1895 assert!(!vr.flip_y);
1896 assert_eq!(vr.rotation_radians, 0.0);
1897 }
1898 other => panic!("Expected GroupBegin, got {:?}", other),
1899 }
1900 }
1901
1902 #[rustfmt::skip]
1903 #[test]
1904 fn test_visual_rotation_preserves_bounding_box() {
1905 let mut ply = Ply::<()>::new_headless(Dimensions::new(800.0, 600.0));
1906 let mut ui = ply.begin();
1907
1908 ui.element()
1909 .width(fixed!(200.0)).height(fixed!(100.0))
1910 .background_color(0xFF0000)
1911 .rotate_visual(|r| r.degrees(45.0))
1912 .empty();
1913
1914 let items = ui.eval();
1915
1916 let rect = &items[1]; assert_eq!(rect.bounding_box.width, 200.0);
1919 assert_eq!(rect.bounding_box.height, 100.0);
1920 }
1921
1922 #[rustfmt::skip]
1923 #[test]
1924 fn test_visual_rotation_config_values() {
1925 let mut ply = Ply::<()>::new_headless(Dimensions::new(800.0, 600.0));
1926 let mut ui = ply.begin();
1927
1928 ui.element()
1929 .width(fixed!(100.0)).height(fixed!(100.0))
1930 .background_color(0xFF0000)
1931 .rotate_visual(|r| r
1932 .radians(std::f32::consts::FRAC_PI_2)
1933 .pivot(0.25, 0.75)
1934 .flip_x()
1935 .flip_y()
1936 )
1937 .empty();
1938
1939 let items = ui.eval();
1940
1941 match &items[0].config {
1942 render_commands::RenderCommandConfig::GroupBegin { visual_rotation, .. } => {
1943 let vr = visual_rotation.as_ref().unwrap();
1944 assert!((vr.rotation_radians - std::f32::consts::FRAC_PI_2).abs() < 0.001);
1945 assert_eq!(vr.pivot_x, 0.25);
1946 assert_eq!(vr.pivot_y, 0.75);
1947 assert!(vr.flip_x);
1948 assert!(vr.flip_y);
1949 }
1950 other => panic!("Expected GroupBegin, got {:?}", other),
1951 }
1952 }
1953
1954 #[rustfmt::skip]
1955 #[test]
1956 fn test_shape_rotation_emits_with_rotation() {
1957 let mut ply = Ply::<()>::new_headless(Dimensions::new(800.0, 600.0));
1958 let mut ui = ply.begin();
1959
1960 ui.element()
1961 .width(fixed!(100.0)).height(fixed!(50.0))
1962 .background_color(0xFF0000)
1963 .rotate_shape(|r| r.degrees(45.0))
1964 .empty();
1965
1966 let items = ui.eval();
1967
1968 assert_eq!(items.len(), 1, "Expected 1 item, got {}", items.len());
1970 let sr = items[0].shape_rotation.as_ref().expect("Should have shape_rotation");
1971 assert!((sr.rotation_radians - 45.0_f32.to_radians()).abs() < 0.001);
1972 assert!(!sr.flip_x);
1973 assert!(!sr.flip_y);
1974 }
1975
1976 #[rustfmt::skip]
1977 #[test]
1978 fn test_shape_rotation_aabb_90_degrees() {
1979 let mut ply = Ply::<()>::new_headless(Dimensions::new(800.0, 600.0));
1981 let mut ui = ply.begin();
1982
1983 ui.element().width(grow!()).height(grow!())
1984 .layout(|l| l)
1985 .children(|ui| {
1986 ui.element()
1987 .width(fixed!(200.0)).height(fixed!(100.0))
1988 .background_color(0xFF0000)
1989 .rotate_shape(|r| r.degrees(90.0))
1990 .empty();
1991 });
1992
1993 let items = ui.eval();
1994
1995 let rect = items.iter().find(|i| matches!(i.config, render_commands::RenderCommandConfig::Rectangle(_))).unwrap();
1997 assert!((rect.bounding_box.width - 200.0).abs() < 0.1, "width should be 200, got {}", rect.bounding_box.width);
1999 assert!((rect.bounding_box.height - 100.0).abs() < 0.1, "height should be 100, got {}", rect.bounding_box.height);
2000 }
2001
2002 #[rustfmt::skip]
2003 #[test]
2004 fn test_shape_rotation_aabb_45_degrees_sharp() {
2005 let mut ply = Ply::<()>::new_headless(Dimensions::new(800.0, 600.0));
2007 let mut ui = ply.begin();
2008
2009 ui.element().width(grow!()).height(grow!())
2011 .layout(|l| l.direction(layout::LayoutDirection::LeftToRight))
2012 .children(|ui| {
2013 ui.element()
2014 .width(fixed!(100.0)).height(fixed!(100.0))
2015 .background_color(0xFF0000)
2016 .rotate_shape(|r| r.degrees(45.0))
2017 .empty();
2018
2019 ui.element()
2021 .width(fixed!(50.0)).height(fixed!(50.0))
2022 .background_color(0x00FF00)
2023 .empty();
2024 });
2025
2026 let items = ui.eval();
2027
2028 let rects: Vec<_> = items.iter()
2030 .filter(|i| matches!(i.config, render_commands::RenderCommandConfig::Rectangle(_)))
2031 .collect();
2032 assert!(rects.len() >= 2, "Expected at least 2 rectangles, got {}", rects.len());
2033
2034 let expected_aabb_w = (2.0_f32.sqrt()) * 100.0; let green_x = rects[1].bounding_box.x;
2036 assert!((green_x - expected_aabb_w).abs() < 1.0,
2038 "Green rect x should be ~{}, got {}", expected_aabb_w, green_x);
2039 }
2040
2041 #[rustfmt::skip]
2042 #[test]
2043 fn test_shape_rotation_aabb_45_degrees_rounded() {
2044 let mut ply = Ply::<()>::new_headless(Dimensions::new(800.0, 600.0));
2047 let mut ui = ply.begin();
2048
2049 ui.element().width(grow!()).height(grow!())
2050 .layout(|l| l.direction(layout::LayoutDirection::LeftToRight))
2051 .children(|ui| {
2052 ui.element()
2053 .width(fixed!(100.0)).height(fixed!(100.0))
2054 .corner_radius(10.0)
2055 .background_color(0xFF0000)
2056 .rotate_shape(|r| r.degrees(45.0))
2057 .empty();
2058
2059 ui.element()
2060 .width(fixed!(50.0)).height(fixed!(50.0))
2061 .background_color(0x00FF00)
2062 .empty();
2063 });
2064
2065 let items = ui.eval();
2066
2067 let rects: Vec<_> = items.iter()
2068 .filter(|i| matches!(i.config, render_commands::RenderCommandConfig::Rectangle(_)))
2069 .collect();
2070 assert!(rects.len() >= 2);
2071
2072 let expected_aabb_w = 80.0 * 2.0_f32.sqrt() + 20.0;
2074 let green_x = rects[1].bounding_box.x;
2075 assert!((green_x - expected_aabb_w).abs() < 1.0,
2077 "Green rect x should be ~{}, got {}", expected_aabb_w, green_x);
2078 }
2079
2080 #[rustfmt::skip]
2081 #[test]
2082 fn test_shape_rotation_noop_no_aabb_change() {
2083 let mut ply = Ply::<()>::new_headless(Dimensions::new(800.0, 600.0));
2085 let mut ui = ply.begin();
2086
2087 ui.element()
2088 .width(fixed!(100.0)).height(fixed!(50.0))
2089 .background_color(0xFF0000)
2090 .rotate_shape(|r| r.degrees(0.0))
2091 .empty();
2092
2093 let items = ui.eval();
2094 assert_eq!(items.len(), 1);
2095 assert_eq!(items[0].bounding_box.width, 100.0);
2096 assert_eq!(items[0].bounding_box.height, 50.0);
2097 assert!(items[0].shape_rotation.is_none(), "Noop shape rotation should be filtered");
2100 }
2101
2102 #[rustfmt::skip]
2103 #[test]
2104 fn test_shape_rotation_flip_only() {
2105 let mut ply = Ply::<()>::new_headless(Dimensions::new(800.0, 600.0));
2107 let mut ui = ply.begin();
2108
2109 ui.element()
2110 .width(fixed!(100.0)).height(fixed!(50.0))
2111 .background_color(0xFF0000)
2112 .rotate_shape(|r| r.flip_x())
2113 .empty();
2114
2115 let items = ui.eval();
2116 assert_eq!(items.len(), 1);
2117 let sr = items[0].shape_rotation.as_ref().expect("flip_x should produce shape_rotation");
2118 assert!(sr.flip_x);
2119 assert!(!sr.flip_y);
2120 assert_eq!(items[0].bounding_box.width, 100.0);
2122 assert_eq!(items[0].bounding_box.height, 50.0);
2123 }
2124
2125 #[rustfmt::skip]
2126 #[test]
2127 fn test_shape_rotation_180_no_aabb_change() {
2128 let mut ply = Ply::<()>::new_headless(Dimensions::new(800.0, 600.0));
2130 let mut ui = ply.begin();
2131
2132 ui.element()
2133 .width(fixed!(200.0)).height(fixed!(100.0))
2134 .background_color(0xFF0000)
2135 .rotate_shape(|r| r.degrees(180.0))
2136 .empty();
2137
2138 let items = ui.eval();
2139 assert_eq!(items.len(), 1);
2140 assert_eq!(items[0].bounding_box.width, 200.0);
2141 assert_eq!(items[0].bounding_box.height, 100.0);
2142 }
2143
2144 #[test]
2145 fn test_classify_angle() {
2146 use math::{classify_angle, AngleType};
2147 assert_eq!(classify_angle(0.0), AngleType::Zero);
2148 assert_eq!(classify_angle(std::f32::consts::TAU), AngleType::Zero);
2149 assert_eq!(classify_angle(-std::f32::consts::TAU), AngleType::Zero);
2150 assert_eq!(classify_angle(std::f32::consts::FRAC_PI_2), AngleType::Right90);
2151 assert_eq!(classify_angle(std::f32::consts::PI), AngleType::Straight180);
2152 assert_eq!(classify_angle(3.0 * std::f32::consts::FRAC_PI_2), AngleType::Right270);
2153 match classify_angle(1.0) {
2154 AngleType::Arbitrary(v) => assert!((v - 1.0).abs() < 0.01),
2155 other => panic!("Expected Arbitrary, got {:?}", other),
2156 }
2157 }
2158
2159 #[test]
2160 fn test_compute_rotated_aabb_zero() {
2161 use math::compute_rotated_aabb;
2162 use layout::CornerRadius;
2163 let cr = CornerRadius::default();
2164 let (w, h) = compute_rotated_aabb(100.0, 50.0, &cr, 0.0);
2165 assert_eq!(w, 100.0);
2166 assert_eq!(h, 50.0);
2167 }
2168
2169 #[test]
2170 fn test_compute_rotated_aabb_90() {
2171 use math::compute_rotated_aabb;
2172 use layout::CornerRadius;
2173 let cr = CornerRadius::default();
2174 let (w, h) = compute_rotated_aabb(200.0, 100.0, &cr, std::f32::consts::FRAC_PI_2);
2175 assert!((w - 100.0).abs() < 0.1, "w should be 100, got {}", w);
2176 assert!((h - 200.0).abs() < 0.1, "h should be 200, got {}", h);
2177 }
2178
2179 #[test]
2180 fn test_compute_rotated_aabb_45_sharp() {
2181 use math::compute_rotated_aabb;
2182 use layout::CornerRadius;
2183 let cr = CornerRadius::default();
2184 let theta = std::f32::consts::FRAC_PI_4;
2185 let (w, h) = compute_rotated_aabb(100.0, 100.0, &cr, theta);
2186 let expected = 100.0 * 2.0_f32.sqrt();
2187 assert!((w - expected).abs() < 0.5, "w should be ~{}, got {}", expected, w);
2188 assert!((h - expected).abs() < 0.5, "h should be ~{}, got {}", expected, h);
2189 }
2190
2191 #[test]
2192 fn test_compute_rotated_aabb_45_rounded() {
2193 use math::compute_rotated_aabb;
2194 use layout::CornerRadius;
2195 let cr = CornerRadius { top_left: 10.0, top_right: 10.0, bottom_left: 10.0, bottom_right: 10.0 };
2196 let theta = std::f32::consts::FRAC_PI_4;
2197 let (w, h) = compute_rotated_aabb(100.0, 100.0, &cr, theta);
2198 let expected = 80.0 * 2.0_f32.sqrt() + 20.0; assert!((w - expected).abs() < 0.5, "w should be ~{}, got {}", expected, w);
2200 assert!((h - expected).abs() < 0.5, "h should be ~{}, got {}", expected, h);
2201 }
2202
2203 #[test]
2204 fn test_on_press_callback_fires() {
2205 use std::cell::RefCell;
2206 use std::rc::Rc;
2207
2208 let mut ply = Ply::<()>::new_headless(Dimensions::new(800.0, 600.0));
2209 let press_count = Rc::new(RefCell::new(0u32));
2210 let release_count = Rc::new(RefCell::new(0u32));
2211
2212 {
2214 let mut ui = ply.begin();
2215 ui.element()
2216 .id("btn")
2217 .width(fixed!(100.0))
2218 .height(fixed!(100.0))
2219 .empty();
2220 ui.eval();
2221 }
2222
2223 {
2225 let pc = press_count.clone();
2226 let rc = release_count.clone();
2227 let mut ui = ply.begin();
2228 ui.element()
2229 .id("btn")
2230 .width(fixed!(100.0))
2231 .height(fixed!(100.0))
2232 .on_press(move |_, _| { *pc.borrow_mut() += 1; })
2233 .on_release(move |_, _| { *rc.borrow_mut() += 1; })
2234 .empty();
2235 ui.eval();
2236 }
2237
2238 ply.context.set_pointer_state(Vector2::new(50.0, 50.0), true);
2240 assert_eq!(*press_count.borrow(), 1, "on_press should fire once");
2241 assert_eq!(*release_count.borrow(), 0, "on_release should not fire yet");
2242
2243 ply.context.set_pointer_state(Vector2::new(50.0, 50.0), false);
2245 assert_eq!(*release_count.borrow(), 1, "on_release should fire once");
2246 }
2247
2248 #[test]
2249 fn test_pressed_query() {
2250 let mut ply = Ply::<()>::new_headless(Dimensions::new(800.0, 600.0));
2251
2252 {
2254 let mut ui = ply.begin();
2255 ui.element()
2256 .id("btn")
2257 .width(fixed!(100.0))
2258 .height(fixed!(100.0))
2259 .empty();
2260 ui.eval();
2261 }
2262
2263 ply.context.set_pointer_state(Vector2::new(50.0, 50.0), true);
2265
2266 {
2268 let mut ui = ply.begin();
2269 ui.element()
2270 .id("btn")
2271 .width(fixed!(100.0))
2272 .height(fixed!(100.0))
2273 .children(|ui| {
2274 assert!(ui.pressed(), "element should report as pressed");
2275 });
2276 ui.eval();
2277 }
2278 }
2279
2280 #[test]
2281 fn test_tab_navigation_cycles_focus() {
2282 let mut ply = Ply::<()>::new_headless(Dimensions::new(800.0, 600.0));
2283
2284 {
2286 let mut ui = ply.begin();
2287 ui.element()
2288 .id("a")
2289 .width(fixed!(100.0))
2290 .height(fixed!(50.0))
2291 .accessibility(|a| a.button("A"))
2292 .empty();
2293 ui.element()
2294 .id("b")
2295 .width(fixed!(100.0))
2296 .height(fixed!(50.0))
2297 .accessibility(|a| a.button("B"))
2298 .empty();
2299 ui.element()
2300 .id("c")
2301 .width(fixed!(100.0))
2302 .height(fixed!(50.0))
2303 .accessibility(|a| a.button("C"))
2304 .empty();
2305 ui.eval();
2306 }
2307
2308 let id_a = Id::from("a").id;
2309 let id_b = Id::from("b").id;
2310 let id_c = Id::from("c").id;
2311
2312 assert_eq!(ply.focused_element(), None);
2314
2315 ply.context.cycle_focus(false);
2317 assert_eq!(ply.context.focused_element_id, id_a);
2318
2319 ply.context.cycle_focus(false);
2321 assert_eq!(ply.context.focused_element_id, id_b);
2322
2323 ply.context.cycle_focus(false);
2325 assert_eq!(ply.context.focused_element_id, id_c);
2326
2327 ply.context.cycle_focus(false);
2329 assert_eq!(ply.context.focused_element_id, id_a);
2330
2331 ply.context.cycle_focus(true);
2333 assert_eq!(ply.context.focused_element_id, id_c);
2334 }
2335
2336 #[test]
2337 fn test_tab_index_ordering() {
2338 let mut ply = Ply::<()>::new_headless(Dimensions::new(800.0, 600.0));
2339
2340 {
2342 let mut ui = ply.begin();
2343 ui.element()
2344 .id("third")
2345 .width(fixed!(100.0))
2346 .height(fixed!(50.0))
2347 .accessibility(|a| a.button("Third").tab_index(3))
2348 .empty();
2349 ui.element()
2350 .id("first")
2351 .width(fixed!(100.0))
2352 .height(fixed!(50.0))
2353 .accessibility(|a| a.button("First").tab_index(1))
2354 .empty();
2355 ui.element()
2356 .id("second")
2357 .width(fixed!(100.0))
2358 .height(fixed!(50.0))
2359 .accessibility(|a| a.button("Second").tab_index(2))
2360 .empty();
2361 ui.eval();
2362 }
2363
2364 let id_first = Id::from("first").id;
2365 let id_second = Id::from("second").id;
2366 let id_third = Id::from("third").id;
2367
2368 ply.context.cycle_focus(false);
2370 assert_eq!(ply.context.focused_element_id, id_first);
2371 ply.context.cycle_focus(false);
2372 assert_eq!(ply.context.focused_element_id, id_second);
2373 ply.context.cycle_focus(false);
2374 assert_eq!(ply.context.focused_element_id, id_third);
2375 }
2376
2377 #[test]
2378 fn test_arrow_key_navigation() {
2379 let mut ply = Ply::<()>::new_headless(Dimensions::new(800.0, 600.0));
2380 use engine::ArrowDirection;
2381
2382 let id_a = Id::from("a").id;
2383 let id_b = Id::from("b").id;
2384
2385 {
2387 let mut ui = ply.begin();
2388 ui.element()
2389 .id("a")
2390 .width(fixed!(100.0))
2391 .height(fixed!(50.0))
2392 .accessibility(|a| a.button("A").focus_right("b"))
2393 .empty();
2394 ui.element()
2395 .id("b")
2396 .width(fixed!(100.0))
2397 .height(fixed!(50.0))
2398 .accessibility(|a| a.button("B").focus_left("a"))
2399 .empty();
2400 ui.eval();
2401 }
2402
2403 ply.context.set_focus(id_a);
2405 assert_eq!(ply.context.focused_element_id, id_a);
2406
2407 ply.context.arrow_focus(ArrowDirection::Right);
2409 assert_eq!(ply.context.focused_element_id, id_b);
2410
2411 ply.context.arrow_focus(ArrowDirection::Left);
2413 assert_eq!(ply.context.focused_element_id, id_a);
2414
2415 ply.context.arrow_focus(ArrowDirection::Up);
2417 assert_eq!(ply.context.focused_element_id, id_a);
2418 }
2419
2420 #[test]
2421 fn test_focused_query() {
2422 let mut ply = Ply::<()>::new_headless(Dimensions::new(800.0, 600.0));
2423
2424 let id_a = Id::from("a").id;
2425
2426 {
2428 let mut ui = ply.begin();
2429 ui.element()
2430 .id("a")
2431 .width(fixed!(100.0))
2432 .height(fixed!(50.0))
2433 .accessibility(|a| a.button("A"))
2434 .empty();
2435 ui.eval();
2436 }
2437
2438 ply.context.set_focus(id_a);
2439
2440 {
2442 let mut ui = ply.begin();
2443 ui.element()
2444 .id("a")
2445 .width(fixed!(100.0))
2446 .height(fixed!(50.0))
2447 .accessibility(|a| a.button("A"))
2448 .children(|ui| {
2449 assert!(ui.focused(), "element should report as focused");
2450 });
2451 ui.eval();
2452 }
2453 }
2454
2455 #[test]
2456 fn test_on_focus_callback_fires_on_tab() {
2457 use std::cell::RefCell;
2458 use std::rc::Rc;
2459
2460 let mut ply = Ply::<()>::new_headless(Dimensions::new(800.0, 600.0));
2461 let focus_a = Rc::new(RefCell::new(0u32));
2462 let unfocus_a = Rc::new(RefCell::new(0u32));
2463 let focus_b = Rc::new(RefCell::new(0u32));
2464
2465 {
2467 let fa = focus_a.clone();
2468 let ua = unfocus_a.clone();
2469 let fb = focus_b.clone();
2470 let mut ui = ply.begin();
2471 ui.element()
2472 .id("a")
2473 .width(fixed!(100.0))
2474 .height(fixed!(50.0))
2475 .accessibility(|a| a.button("A"))
2476 .on_focus(move |_| { *fa.borrow_mut() += 1; })
2477 .on_unfocus(move |_| { *ua.borrow_mut() += 1; })
2478 .empty();
2479 ui.element()
2480 .id("b")
2481 .width(fixed!(100.0))
2482 .height(fixed!(50.0))
2483 .accessibility(|a| a.button("B"))
2484 .on_focus(move |_| { *fb.borrow_mut() += 1; })
2485 .empty();
2486 ui.eval();
2487 }
2488
2489 ply.context.cycle_focus(false);
2491 assert_eq!(*focus_a.borrow(), 1, "on_focus should fire for A");
2492 assert_eq!(*unfocus_a.borrow(), 0, "on_unfocus should not fire yet");
2493
2494 ply.context.cycle_focus(false);
2496 assert_eq!(*unfocus_a.borrow(), 1, "on_unfocus should fire for A");
2497 assert_eq!(*focus_b.borrow(), 1, "on_focus should fire for B");
2498 }
2499
2500 #[test]
2501 fn test_on_focus_callback_fires_on_set_focus() {
2502 use std::cell::RefCell;
2503 use std::rc::Rc;
2504
2505 let mut ply = Ply::<()>::new_headless(Dimensions::new(800.0, 600.0));
2506 let focus_count = Rc::new(RefCell::new(0u32));
2507 let unfocus_count = Rc::new(RefCell::new(0u32));
2508
2509 let id_a = Id::from("a").id;
2510
2511 {
2513 let fc = focus_count.clone();
2514 let uc = unfocus_count.clone();
2515 let mut ui = ply.begin();
2516 ui.element()
2517 .id("a")
2518 .width(fixed!(100.0))
2519 .height(fixed!(50.0))
2520 .accessibility(|a| a.button("A"))
2521 .on_focus(move |_| { *fc.borrow_mut() += 1; })
2522 .on_unfocus(move |_| { *uc.borrow_mut() += 1; })
2523 .empty();
2524 ui.eval();
2525 }
2526
2527 ply.context.set_focus(id_a);
2529 assert_eq!(*focus_count.borrow(), 1, "on_focus should fire on set_focus");
2530
2531 ply.context.clear_focus();
2533 assert_eq!(*unfocus_count.borrow(), 1, "on_unfocus should fire on clear_focus");
2534 }
2535
2536 #[test]
2537 fn test_focus_ring_render_command() {
2538 use render_commands::RenderCommandConfig;
2539
2540 let mut ply = Ply::<()>::new_headless(Dimensions::new(800.0, 600.0));
2541 let id_a = Id::from("a").id;
2542
2543 {
2545 let mut ui = ply.begin();
2546 ui.element()
2547 .id("a")
2548 .width(fixed!(100.0))
2549 .height(fixed!(50.0))
2550 .corner_radius(8.0)
2551 .accessibility(|a| a.button("A"))
2552 .empty();
2553 ui.eval();
2554 }
2555
2556 ply.context.focus_from_keyboard = true;
2558 ply.context.set_focus(id_a);
2559
2560 {
2562 let mut ui = ply.begin();
2563 ui.element()
2564 .id("a")
2565 .width(fixed!(100.0))
2566 .height(fixed!(50.0))
2567 .corner_radius(8.0)
2568 .accessibility(|a| a.button("A"))
2569 .empty();
2570 let items = ui.eval();
2571
2572 let focus_ring = items.iter().find(|cmd| {
2574 cmd.z_index == 32764 && matches!(cmd.config, RenderCommandConfig::Border(_))
2575 });
2576 assert!(focus_ring.is_some(), "Focus ring border should be in render commands");
2577
2578 let ring = focus_ring.unwrap();
2579 assert!(ring.bounding_box.width > 100.0, "Focus ring should be wider than element");
2581 assert!(ring.bounding_box.height > 50.0, "Focus ring should be taller than element");
2582 }
2583 }
2584}