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