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 easing;
9pub mod elements;
10pub mod engine;
11pub mod id;
12pub mod lerp;
13pub mod layout;
14pub mod math;
15pub mod render_commands;
16pub mod shader_build;
17pub mod shaders;
18pub mod text;
19pub mod text_input;
20pub mod renderer;
21#[cfg(feature = "text-styling")]
22pub mod text_styling;
23#[cfg(feature = "built-in-shaders")]
24pub mod built_in_shaders;
25#[cfg(feature = "net")]
26pub mod net;
27#[cfg(feature = "storage")]
28pub mod storage;
29pub mod jobs;
30pub mod prelude;
31
32use id::Id;
33use math::{Dimensions, Vector2};
34use render_commands::RenderCommand;
35#[cfg(feature = "a11y")]
36use rustc_hash::FxHashMap;
37use text::TextConfig;
38
39pub use color::Color;
40
41#[allow(dead_code)]
42pub struct Ply<CustomElementData: Clone + Default + std::fmt::Debug = ()> {
43 context: engine::PlyContext<CustomElementData>,
44 headless: bool,
45 text_input_repeat_key: u32,
47 text_input_repeat_first: f64,
48 text_input_repeat_last: f64,
49 text_input_repeat_focus_id: u32,
52 was_text_input_focused: bool,
54 #[cfg(all(feature = "a11y", target_arch = "wasm32"))]
55 web_a11y_state: accessibility_web::WebAccessibilityState,
56 #[cfg(all(feature = "a11y", not(target_arch = "wasm32")))]
57 native_a11y_state: accessibility_native::NativeAccessibilityState,
58}
59
60pub struct Ui<'ply, CustomElementData: Clone + Default + std::fmt::Debug = ()> {
61 ply: &'ply mut Ply<CustomElementData>,
62}
63
64#[must_use]
67pub struct ElementBuilder<'ply, CustomElementData: Clone + Default + std::fmt::Debug = ()> {
68 ply: &'ply mut Ply<CustomElementData>,
69 inner: engine::ElementDeclaration<CustomElementData>,
70 id: Option<Id>,
71 on_hover_fn: Option<Box<dyn FnMut(Id, engine::PointerData) + 'static>>,
72 on_press_fn: Option<Box<dyn FnMut(Id, engine::PointerData) + 'static>>,
73 on_release_fn: Option<Box<dyn FnMut(Id, engine::PointerData) + 'static>>,
74 on_focus_fn: Option<Box<dyn FnMut(Id) + 'static>>,
75 on_unfocus_fn: Option<Box<dyn FnMut(Id) + 'static>>,
76 text_input_on_changed_fn: Option<Box<dyn FnMut(&str) + 'static>>,
77 text_input_on_submit_fn: Option<Box<dyn FnMut(&str) + 'static>>,
78}
79
80impl<'ply, CustomElementData: Clone + Default + std::fmt::Debug>
81 ElementBuilder<'ply, CustomElementData>
82{
83 #[inline]
85 pub fn width(mut self, width: layout::Sizing) -> Self {
86 self.inner.layout.sizing.width = width.into();
87 self
88 }
89
90 #[inline]
92 pub fn height(mut self, height: layout::Sizing) -> Self {
93 self.inner.layout.sizing.height = height.into();
94 self
95 }
96
97 #[inline]
99 pub fn background_color(mut self, color: impl Into<Color>) -> Self {
100 self.inner.background_color = color.into();
101 self
102 }
103
104 #[inline]
107 pub fn corner_radius(mut self, radius: impl Into<layout::CornerRadius>) -> Self {
108 self.inner.corner_radius = radius.into();
109 self
110 }
111
112 #[inline]
116 pub fn id(mut self, id: impl Into<Id>) -> Self {
117 self.id = Some(id.into());
118 self
119 }
120
121 #[inline]
123 pub fn aspect_ratio(mut self, aspect_ratio: f32) -> Self {
124 self.inner.aspect_ratio = aspect_ratio;
125 self
126 }
127
128 #[inline]
130 pub fn contain(mut self, aspect_ratio: f32) -> Self {
131 self.inner.layout.sizing.width = layout::Sizing::Grow(0.0, f32::MAX, 1.0).into();
132 self.inner.layout.sizing.height = layout::Sizing::Grow(0.0, f32::MAX, 1.0).into();
133 self.inner.aspect_ratio = aspect_ratio;
134 self.inner.cover_aspect_ratio = false;
135 self
136 }
137
138 #[inline]
140 pub fn cover(mut self, aspect_ratio: f32) -> Self {
141 self.inner.layout.sizing.width = layout::Sizing::Grow(0.0, f32::MAX, 1.0).into();
142 self.inner.layout.sizing.height = layout::Sizing::Grow(0.0, f32::MAX, 1.0).into();
143 self.inner.aspect_ratio = aspect_ratio;
144 self.inner.cover_aspect_ratio = true;
145 self
146 }
147
148 #[inline]
150 pub fn overflow(mut self, f: impl for<'a> FnOnce(&'a mut elements::OverflowBuilder) -> &'a mut elements::OverflowBuilder) -> Self {
151 let mut builder = elements::OverflowBuilder { config: self.inner.clip };
152 f(&mut builder);
153 self.inner.clip = builder.config;
154 self
155 }
156
157 #[inline]
159 pub fn custom_element(mut self, data: CustomElementData) -> Self {
160 self.inner.custom_data = Some(data);
161 self
162 }
163
164 #[inline]
166 pub fn layout(mut self, f: impl for<'a> FnOnce(&'a mut layout::LayoutBuilder) -> &'a mut layout::LayoutBuilder) -> Self {
167 let mut builder = layout::LayoutBuilder { config: self.inner.layout };
168 f(&mut builder);
169 self.inner.layout = builder.config;
170 self
171 }
172
173 #[inline]
175 pub fn floating(mut self, f: impl for<'a> FnOnce(&'a mut elements::FloatingBuilder) -> &'a mut elements::FloatingBuilder) -> Self {
176 let mut builder = elements::FloatingBuilder { config: self.inner.floating };
177 f(&mut builder);
178 self.inner.floating = builder.config;
179 self
180 }
181
182 #[inline]
184 pub fn border(mut self, f: impl for<'a> FnOnce(&'a mut elements::BorderBuilder) -> &'a mut elements::BorderBuilder) -> Self {
185 let mut builder = elements::BorderBuilder { config: self.inner.border };
186 f(&mut builder);
187 self.inner.border = builder.config;
188 self
189 }
190
191 #[inline]
198 pub fn image(mut self, data: impl Into<renderer::ImageSource>) -> Self {
199 self.inner.image_data = Some(data.into());
200 self
201 }
202
203 #[inline]
218 pub fn effect(mut self, asset: &shaders::ShaderAsset, f: impl FnOnce(&mut shaders::ShaderBuilder<'_>)) -> Self {
219 let mut builder = shaders::ShaderBuilder::new(asset);
220 f(&mut builder);
221 self.inner.effects.push(builder.into_config());
222 self
223 }
224
225 #[inline]
244 pub fn shader(mut self, asset: &shaders::ShaderAsset, f: impl FnOnce(&mut shaders::ShaderBuilder<'_>)) -> Self {
245 let mut builder = shaders::ShaderBuilder::new(asset);
246 f(&mut builder);
247 self.inner.shaders.push(builder.into_config());
248 self
249 }
250
251 #[inline]
272 pub fn rotate_visual(mut self, f: impl for<'a> FnOnce(&'a mut elements::VisualRotationBuilder) -> &'a mut elements::VisualRotationBuilder) -> Self {
273 let mut builder = elements::VisualRotationBuilder {
274 config: engine::VisualRotationConfig::default(),
275 };
276 f(&mut builder);
277 self.inner.visual_rotation = Some(builder.config);
278 self
279 }
280
281 #[inline]
297 pub fn rotate_shape(mut self, f: impl for<'a> FnOnce(&'a mut elements::ShapeRotationBuilder) -> &'a mut elements::ShapeRotationBuilder) -> Self {
298 let mut builder = elements::ShapeRotationBuilder {
299 config: engine::ShapeRotationConfig::default(),
300 };
301 f(&mut builder);
302 self.inner.shape_rotation = Some(builder.config);
303 self
304 }
305
306 #[inline]
319 pub fn accessibility(
320 mut self,
321 f: impl for<'a> FnOnce(&'a mut accessibility::AccessibilityBuilder) -> &'a mut accessibility::AccessibilityBuilder,
322 ) -> Self {
323 let mut builder = accessibility::AccessibilityBuilder::new();
324 f(&mut builder);
325 self.inner.accessibility = Some(builder.config);
326 self
327 }
328
329 #[inline]
332 pub fn preserve_focus(mut self) -> Self {
333 self.inner.preserve_focus = true;
334 self
335 }
336
337 #[inline]
339 pub fn on_hover<F>(mut self, callback: F) -> Self
340 where
341 F: FnMut(Id, engine::PointerData) + 'static,
342 {
343 self.on_hover_fn = Some(Box::new(callback));
344 self
345 }
346
347 #[inline]
350 pub fn on_press<F>(mut self, callback: F) -> Self
351 where
352 F: FnMut(Id, engine::PointerData) + 'static,
353 {
354 self.on_press_fn = Some(Box::new(callback));
355 self
356 }
357
358 #[inline]
361 pub fn on_release<F>(mut self, callback: F) -> Self
362 where
363 F: FnMut(Id, engine::PointerData) + 'static,
364 {
365 self.on_release_fn = Some(Box::new(callback));
366 self
367 }
368
369 #[inline]
372 pub fn on_focus<F>(mut self, callback: F) -> Self
373 where
374 F: FnMut(Id) + 'static,
375 {
376 self.on_focus_fn = Some(Box::new(callback));
377 self
378 }
379
380 #[inline]
382 pub fn on_unfocus<F>(mut self, callback: F) -> Self
383 where
384 F: FnMut(Id) + 'static,
385 {
386 self.on_unfocus_fn = Some(Box::new(callback));
387 self
388 }
389
390 #[inline]
409 pub fn text_input(
410 mut self,
411 f: impl for<'a> FnOnce(&'a mut text_input::TextInputBuilder) -> &'a mut text_input::TextInputBuilder,
412 ) -> Self {
413 let mut builder = text_input::TextInputBuilder::new();
414 f(&mut builder);
415 self.inner.text_input = Some(builder.config);
416 self.text_input_on_changed_fn = builder.on_changed_fn;
417 self.text_input_on_submit_fn = builder.on_submit_fn;
418 self
419 }
420
421 pub fn children(self, f: impl FnOnce(&mut Ui<'_, CustomElementData>)) -> Id {
423 let ElementBuilder {
424 ply, inner, id,
425 on_hover_fn, on_press_fn, on_release_fn, on_focus_fn, on_unfocus_fn,
426 text_input_on_changed_fn, text_input_on_submit_fn,
427 } = self;
428 if let Some(ref id) = id {
429 ply.context.open_element_with_id(id);
430 } else {
431 ply.context.open_element();
432 }
433 ply.context.configure_open_element(&inner);
434 let element_id = ply.context.get_open_element_id();
435
436 if let Some(hover_fn) = on_hover_fn {
437 ply.context.on_hover(hover_fn);
438 }
439 if on_press_fn.is_some() || on_release_fn.is_some() {
440 ply.context.set_press_callbacks(on_press_fn, on_release_fn);
441 }
442 if on_focus_fn.is_some() || on_unfocus_fn.is_some() {
443 ply.context.set_focus_callbacks(on_focus_fn, on_unfocus_fn);
444 }
445 if text_input_on_changed_fn.is_some() || text_input_on_submit_fn.is_some() {
446 ply.context.set_text_input_callbacks(text_input_on_changed_fn, text_input_on_submit_fn);
447 }
448
449 let mut ui = Ui { ply };
450 f(&mut ui);
451 ui.ply.context.close_element();
452
453 Id { id: element_id, ..Default::default() }
454 }
455
456 pub fn empty(self) -> Id {
458 self.children(|_| {})
459 }
460}
461
462impl<'ply, CustomElementData: Clone + Default + std::fmt::Debug> core::ops::Deref
463 for Ui<'ply, CustomElementData>
464{
465 type Target = Ply<CustomElementData>;
466
467 fn deref(&self) -> &Self::Target {
468 self.ply
469 }
470}
471
472impl<'ply, CustomElementData: Clone + Default + std::fmt::Debug> core::ops::DerefMut
473 for Ui<'ply, CustomElementData>
474{
475 fn deref_mut(&mut self) -> &mut Self::Target {
476 self.ply
477 }
478}
479
480impl<'ply, CustomElementData: Clone + Default + std::fmt::Debug> Ui<'ply, CustomElementData> {
481 pub fn element(&mut self) -> ElementBuilder<'_, CustomElementData> {
484 ElementBuilder {
485 ply: &mut *self.ply,
486 inner: engine::ElementDeclaration::default(),
487 id: None,
488 on_hover_fn: None,
489 on_press_fn: None,
490 on_release_fn: None,
491 on_focus_fn: None,
492 on_unfocus_fn: None,
493 text_input_on_changed_fn: None,
494 text_input_on_submit_fn: None,
495 }
496 }
497
498 pub fn text(&mut self, text: &str, config_fn: impl FnOnce(&mut TextConfig) -> &mut TextConfig) {
500 let mut config = TextConfig::new();
501 config_fn(&mut config);
502 let text_config_index = self.ply.context.store_text_element_config(config);
503 self.ply.context.open_text_element(text, text_config_index);
504 }
505
506 pub fn scroll_offset(&self) -> Vector2 {
508 self.ply.context.get_scroll_offset()
509 }
510
511 pub fn hovered(&self) -> bool {
513 self.ply.context.hovered()
514 }
515
516 pub fn pressed(&self) -> bool {
519 self.ply.context.pressed()
520 }
521
522 pub fn just_pressed(&self) -> bool {
524 self.ply.context.just_pressed()
525 }
526
527 pub fn just_released(&self) -> bool {
529 self.ply.context.just_released()
530 }
531
532 pub fn focused(&self) -> bool {
534 self.ply.context.focused()
535 }
536}
537
538impl<CustomElementData: Clone + Default + std::fmt::Debug> Ply<CustomElementData> {
539 #[cfg(feature = "a11y")]
540 fn accessibility_bounds(&self) -> FxHashMap<u32, math::BoundingBox> {
541 let mut accessibility_bounds = FxHashMap::default();
542 for &elem_id in &self.context.accessibility_element_order {
543 if let Some(bounds) = self.context.get_element_data(Id {
544 id: elem_id,
545 ..Default::default()
546 }) {
547 accessibility_bounds.insert(elem_id, bounds);
548 }
549 }
550 accessibility_bounds
551 }
552
553 pub fn begin(
555 &mut self,
556 ) -> Ui<'_, CustomElementData> {
557 jobs::poll_completions();
558
559 if !self.headless {
560 self.context.set_layout_dimensions(Dimensions::new(
561 macroquad::prelude::screen_width(),
562 macroquad::prelude::screen_height(),
563 ));
564
565 self.context.current_time = macroquad::prelude::get_time();
567 self.context.frame_delta_time = macroquad::prelude::get_frame_time();
568 }
569
570 self.context.update_text_input_blink_timers();
572
573 if !self.headless {
575 let (mx, my) = macroquad::prelude::mouse_position();
576 let pointer_pos = Vector2::new(mx, my);
577 let is_down = macroquad::prelude::is_mouse_button_down(
578 macroquad::prelude::MouseButton::Left,
579 );
580 let pressed_this_frame = macroquad::prelude::is_mouse_button_pressed(
581 macroquad::prelude::MouseButton::Left,
582 );
583 let released_this_frame = macroquad::prelude::is_mouse_button_released(
584 macroquad::prelude::MouseButton::Left,
585 );
586
587 match (pressed_this_frame, released_this_frame) {
588 (true, true) => {
589 if is_down {
590 self.context.set_pointer_state(pointer_pos, false);
591 self.context.set_pointer_state(pointer_pos, true);
592 } else {
593 self.context.set_pointer_state(pointer_pos, true);
594 self.context.set_pointer_state(pointer_pos, false);
595 }
596 }
597 (true, false) => self.context.set_pointer_state(pointer_pos, true),
598 (false, true) => self.context.set_pointer_state(pointer_pos, false),
599 (false, false) => self.context.set_pointer_state(pointer_pos, is_down),
600 }
601
602 {
603 use macroquad::prelude::{is_key_down, KeyCode};
604 let shift = is_key_down(KeyCode::LeftShift) || is_key_down(KeyCode::RightShift);
605 if shift {
606 if let Some(ref mut pending) = self.context.pending_text_click {
608 pending.3 = true;
609 }
610 }
611 }
612
613 let (scroll_x, scroll_y) = macroquad::prelude::mouse_wheel();
614 #[cfg(target_arch = "wasm32")]
615 const SCROLL_SPEED: f32 = 1.0;
616 #[cfg(not(target_arch = "wasm32"))]
617 const SCROLL_SPEED: f32 = 20.0;
618 let scroll_shift = {
620 use macroquad::prelude::{is_key_down, KeyCode};
621 is_key_down(KeyCode::LeftShift) || is_key_down(KeyCode::RightShift)
622 };
623 let scroll_delta = if scroll_shift {
624 Vector2::new(
626 (scroll_x + scroll_y) * SCROLL_SPEED,
627 0.0,
628 )
629 } else {
630 Vector2::new(scroll_x * SCROLL_SPEED, scroll_y * SCROLL_SPEED)
631 };
632 let touch_input_active = !macroquad::prelude::touches().is_empty();
633
634 let text_consumed_scroll = self.context.update_text_input_pointer_scroll(
636 scroll_delta,
637 touch_input_active,
638 );
639 self.context.clamp_text_input_scroll();
640
641 let container_scroll = if text_consumed_scroll {
643 Vector2::new(0.0, 0.0)
644 } else {
645 scroll_delta
646 };
647 self.context.update_scroll_containers(
648 true,
649 container_scroll,
650 macroquad::prelude::get_frame_time(),
651 touch_input_active,
652 );
653
654 use macroquad::prelude::{is_key_pressed, is_key_down, is_key_released, KeyCode};
656
657 let text_input_focused = self.context.is_text_input_focused();
658 let current_focused_id = self.context.focused_element_id;
659
660 if current_focused_id != self.text_input_repeat_focus_id {
663 self.text_input_repeat_key = 0;
664 self.text_input_repeat_focus_id = current_focused_id;
665 }
666
667 if is_key_pressed(KeyCode::Tab) {
669 let shift = is_key_down(KeyCode::LeftShift) || is_key_down(KeyCode::RightShift);
670 self.context.cycle_focus(shift);
671 } else if text_input_focused {
672 let shift = is_key_down(KeyCode::LeftShift) || is_key_down(KeyCode::RightShift);
674 let ctrl = is_key_down(KeyCode::LeftControl) || is_key_down(KeyCode::RightControl);
675 let time = self.context.current_time;
676
677 const INITIAL_DELAY: f64 = 0.5;
679 const REPEAT_INTERVAL: f64 = 0.033;
680
681 macro_rules! key_fires {
683 ($key:expr, $id:expr) => {{
684 if is_key_pressed($key) {
685 self.text_input_repeat_key = $id;
686 self.text_input_repeat_first = time;
687 self.text_input_repeat_last = time;
688 true
689 } else if is_key_down($key) && self.text_input_repeat_key == $id {
690 let since_first = time - self.text_input_repeat_first;
691 let since_last = time - self.text_input_repeat_last;
692 if since_first > INITIAL_DELAY && since_last > REPEAT_INTERVAL {
693 self.text_input_repeat_last = time;
694 true
695 } else {
696 false
697 }
698 } else {
699 false
700 }
701 }};
702 }
703
704 let mut cursor_moved = false;
706 if key_fires!(KeyCode::Left, 1) {
707 if ctrl {
708 self.context.process_text_input_action(engine::TextInputAction::MoveWordLeft { shift });
709 } else {
710 self.context.process_text_input_action(engine::TextInputAction::MoveLeft { shift });
711 }
712 cursor_moved = true;
713 }
714 if key_fires!(KeyCode::Right, 2) {
715 if ctrl {
716 self.context.process_text_input_action(engine::TextInputAction::MoveWordRight { shift });
717 } else {
718 self.context.process_text_input_action(engine::TextInputAction::MoveRight { shift });
719 }
720 cursor_moved = true;
721 }
722 if key_fires!(KeyCode::Backspace, 3) {
723 if ctrl {
724 self.context.process_text_input_action(engine::TextInputAction::BackspaceWord);
725 } else {
726 self.context.process_text_input_action(engine::TextInputAction::Backspace);
727 }
728 cursor_moved = true;
729 }
730 if key_fires!(KeyCode::Delete, 4) {
731 if ctrl {
732 self.context.process_text_input_action(engine::TextInputAction::DeleteWord);
733 } else {
734 self.context.process_text_input_action(engine::TextInputAction::Delete);
735 }
736 cursor_moved = true;
737 }
738 if key_fires!(KeyCode::Home, 5) {
739 self.context.process_text_input_action(engine::TextInputAction::MoveHome { shift });
740 cursor_moved = true;
741 }
742 if key_fires!(KeyCode::End, 6) {
743 self.context.process_text_input_action(engine::TextInputAction::MoveEnd { shift });
744 cursor_moved = true;
745 }
746
747 if self.context.is_focused_text_input_multiline() {
749 if key_fires!(KeyCode::Up, 7) {
750 self.context.process_text_input_action(engine::TextInputAction::MoveUp { shift });
751 cursor_moved = true;
752 }
753 if key_fires!(KeyCode::Down, 8) {
754 self.context.process_text_input_action(engine::TextInputAction::MoveDown { shift });
755 cursor_moved = true;
756 }
757 }
758
759 if is_key_pressed(KeyCode::Enter) {
761 self.context.process_text_input_action(engine::TextInputAction::Submit);
762 cursor_moved = true;
763 }
764 if ctrl && is_key_pressed(KeyCode::A) {
765 self.context.process_text_input_action(engine::TextInputAction::SelectAll);
766 }
767 if ctrl && is_key_pressed(KeyCode::Z) {
768 if shift {
769 self.context.process_text_input_action(engine::TextInputAction::Redo);
770 } else {
771 self.context.process_text_input_action(engine::TextInputAction::Undo);
772 }
773 cursor_moved = true;
774 }
775 if ctrl && is_key_pressed(KeyCode::Y) {
776 self.context.process_text_input_action(engine::TextInputAction::Redo);
777 cursor_moved = true;
778 }
779 if ctrl && is_key_pressed(KeyCode::C) {
780 let elem_id = self.context.focused_element_id;
782 if let Some(state) = self.context.text_edit_states.get(&elem_id) {
783 #[cfg(feature = "text-styling")]
784 let selected = state.selected_text_styled();
785 #[cfg(not(feature = "text-styling"))]
786 let selected = state.selected_text().to_string();
787 if !selected.is_empty() {
788 macroquad::miniquad::window::clipboard_set(&selected);
789 }
790 }
791 }
792 if ctrl && is_key_pressed(KeyCode::X) {
793 let elem_id = self.context.focused_element_id;
795 if let Some(state) = self.context.text_edit_states.get(&elem_id) {
796 #[cfg(feature = "text-styling")]
797 let selected = state.selected_text_styled();
798 #[cfg(not(feature = "text-styling"))]
799 let selected = state.selected_text().to_string();
800 if !selected.is_empty() {
801 macroquad::miniquad::window::clipboard_set(&selected);
802 }
803 }
804 self.context.process_text_input_action(engine::TextInputAction::Cut);
805 cursor_moved = true;
806 }
807 if ctrl && is_key_pressed(KeyCode::V) {
808 if let Some(text) = macroquad::miniquad::window::clipboard_get() {
810 self.context.process_text_input_action(engine::TextInputAction::Paste { text });
811 cursor_moved = true;
812 }
813 }
814
815 if is_key_pressed(KeyCode::Escape) {
817 self.context.clear_focus();
818 }
819
820 if self.text_input_repeat_key != 0 {
822 let still_down = match self.text_input_repeat_key {
823 1 => is_key_down(KeyCode::Left),
824 2 => is_key_down(KeyCode::Right),
825 3 => is_key_down(KeyCode::Backspace),
826 4 => is_key_down(KeyCode::Delete),
827 5 => is_key_down(KeyCode::Home),
828 6 => is_key_down(KeyCode::End),
829 7 => is_key_down(KeyCode::Up),
830 8 => is_key_down(KeyCode::Down),
831 _ => false,
832 };
833 if !still_down {
834 self.text_input_repeat_key = 0;
835 }
836 }
837
838 while let Some(ch) = macroquad::prelude::get_char_pressed() {
840 if !ch.is_control() && !ctrl {
842 self.context.process_text_input_char(ch);
843 cursor_moved = true;
844 }
845 }
846
847 if cursor_moved {
850 self.context.update_text_input_scroll();
851 }
852 self.context.clamp_text_input_scroll();
853 } else {
854 if is_key_pressed(KeyCode::Right) { self.context.arrow_focus(engine::ArrowDirection::Right); }
856 if is_key_pressed(KeyCode::Left) { self.context.arrow_focus(engine::ArrowDirection::Left); }
857 if is_key_pressed(KeyCode::Up) { self.context.arrow_focus(engine::ArrowDirection::Up); }
858 if is_key_pressed(KeyCode::Down) { self.context.arrow_focus(engine::ArrowDirection::Down); }
859
860 let activate_pressed = is_key_pressed(KeyCode::Enter) || is_key_pressed(KeyCode::Space);
861 let activate_released = is_key_released(KeyCode::Enter) || is_key_released(KeyCode::Space);
862 self.context.handle_keyboard_activation(activate_pressed, activate_released);
863 }
864 }
865
866 {
868 let text_input_focused = self.context.is_text_input_focused();
869 if text_input_focused != self.was_text_input_focused {
870 #[cfg(not(any(target_arch = "wasm32", target_os = "linux")))]
871 {
872 macroquad::miniquad::window::show_keyboard(text_input_focused);
873 }
874 #[cfg(target_arch = "wasm32")]
875 {
876 unsafe { ply_show_virtual_keyboard(text_input_focused); }
877 }
878 self.was_text_input_focused = text_input_focused;
879 }
880 }
881
882 self.context.begin_layout();
883 Ui {
884 ply: self,
885 }
886 }
887
888 pub async fn new(default_font: &'static renderer::FontAsset) -> Self {
890 renderer::FontManager::load_default(default_font).await;
891
892 let dimensions = Dimensions::new(
893 macroquad::prelude::screen_width(),
894 macroquad::prelude::screen_height(),
895 );
896 let mut ply = Self {
897 context: engine::PlyContext::new(dimensions),
898 headless: false,
899 text_input_repeat_key: 0,
900 text_input_repeat_first: 0.0,
901 text_input_repeat_last: 0.0,
902 text_input_repeat_focus_id: 0,
903 was_text_input_focused: false,
904 #[cfg(all(feature = "a11y", target_arch = "wasm32"))]
905 web_a11y_state: accessibility_web::WebAccessibilityState::default(),
906 #[cfg(all(feature = "a11y", not(target_arch = "wasm32")))]
907 native_a11y_state: accessibility_native::NativeAccessibilityState::default(),
908 };
909 ply.context.default_font_key = default_font.key();
910 ply.set_measure_text_function(renderer::create_measure_text_function());
911 ply
912 }
913
914 pub fn new_headless(dimensions: Dimensions) -> Self {
919 Self {
920 context: engine::PlyContext::new(dimensions),
921 headless: true,
922 text_input_repeat_key: 0,
923 text_input_repeat_first: 0.0,
924 text_input_repeat_last: 0.0,
925 text_input_repeat_focus_id: 0,
926 was_text_input_focused: false,
927 #[cfg(all(feature = "a11y", target_arch = "wasm32"))]
928 web_a11y_state: accessibility_web::WebAccessibilityState::default(),
929 #[cfg(all(feature = "a11y", not(target_arch = "wasm32")))]
930 native_a11y_state: accessibility_native::NativeAccessibilityState::default(),
931 }
932 }
933
934 pub fn pointer_over(&self, cfg: impl Into<Id>) -> bool {
936 self.context.pointer_over(cfg.into())
937 }
938
939 pub fn pointer_over_ids(&self) -> Vec<Id> {
941 self.context.get_pointer_over_ids().to_vec()
942 }
943
944 pub fn set_measure_text_function<F>(&mut self, callback: F)
946 where
947 F: Fn(&str, &TextConfig) -> Dimensions + 'static,
948 {
949 self.context.set_measure_text_function(Box::new(
950 move |text: &str, config: &TextConfig| -> Dimensions {
951 callback(text, config)
952 },
953 ));
954 }
955
956 pub fn max_element_count(&mut self, max_element_count: u32) {
959 self.context.set_max_element_count(max_element_count as i32);
960 }
961
962 pub fn max_measure_text_cache_word_count(&mut self, count: u32) {
965 self.context.set_max_measure_text_cache_word_count(count as i32);
966 }
967
968 pub fn set_debug_mode(&mut self, enable: bool) {
970 self.context.set_debug_mode_enabled(enable);
971 }
972
973 pub fn set_debug_view_width(&mut self, width: f32) {
975 self.context.set_debug_view_width(width);
976 }
977
978 pub fn is_debug_mode(&self) -> bool {
980 self.context.is_debug_mode_enabled()
981 }
982
983 pub fn set_culling(&mut self, enable: bool) {
985 self.context.set_culling_enabled(enable);
986 }
987
988 pub fn set_layout_dimensions(&mut self, dimensions: Dimensions) {
991 self.context.set_layout_dimensions(dimensions);
992 }
993
994 pub fn pointer_state(&mut self, position: Vector2, is_down: bool) {
997 self.context.set_pointer_state(position, is_down);
998 }
999
1000 pub fn update_scroll_containers(
1002 &mut self,
1003 drag_scrolling_enabled: bool,
1004 scroll_delta: Vector2,
1005 delta_time: f32,
1006 ) {
1007 let touch_input_active = if self.headless {
1008 false
1009 } else {
1010 !macroquad::prelude::touches().is_empty()
1011 };
1012 self.context
1013 .update_scroll_containers(drag_scrolling_enabled, scroll_delta, delta_time, touch_input_active);
1014 }
1015
1016 pub fn focused_element(&self) -> Option<Id> {
1018 self.context.focused_element()
1019 }
1020
1021 pub fn set_focus(&mut self, id: impl Into<Id>) {
1023 self.context.set_focus(id.into().id);
1024 }
1025
1026 pub fn clear_focus(&mut self) {
1028 self.context.clear_focus();
1029 }
1030
1031 pub fn get_text_value(&self, id: impl Into<Id>) -> &str {
1034 self.context.get_text_value(id.into().id)
1035 }
1036
1037 pub fn set_text_value(&mut self, id: impl Into<Id>, value: &str) {
1039 self.context.set_text_value(id.into().id, value);
1040 }
1041
1042 pub fn get_cursor_pos(&self, id: impl Into<Id>) -> usize {
1045 self.context.get_cursor_pos(id.into().id)
1046 }
1047
1048 pub fn set_cursor_pos(&mut self, id: impl Into<Id>, pos: usize) {
1051 self.context.set_cursor_pos(id.into().id, pos);
1052 }
1053
1054 pub fn get_selection_range(&self, id: impl Into<Id>) -> Option<(usize, usize)> {
1056 self.context.get_selection_range(id.into().id)
1057 }
1058
1059 pub fn set_selection(&mut self, id: impl Into<Id>, anchor: usize, cursor: usize) {
1062 self.context.set_selection(id.into().id, anchor, cursor);
1063 }
1064
1065 pub fn is_pressed(&self, id: impl Into<Id>) -> bool {
1067 self.context.is_element_pressed(id.into().id)
1068 }
1069
1070 pub fn is_just_pressed(&self, id: impl Into<Id>) -> bool {
1072 self.context.is_element_just_pressed(id.into().id)
1073 }
1074
1075 pub fn is_just_released(&self, id: impl Into<Id>) -> bool {
1077 self.context.is_element_just_released(id.into().id)
1078 }
1079
1080 pub fn bounding_box(&self, id: impl Into<Id>) -> Option<math::BoundingBox> {
1082 self.context.get_element_data(id.into())
1083 }
1084
1085 pub fn scroll_container_data(&self, id: impl Into<Id>) -> Option<engine::ScrollContainerData> {
1087 let data = self.context.get_scroll_container_data(id.into());
1088 if data.found {
1089 Some(data)
1090 } else {
1091 None
1092 }
1093 }
1094
1095 pub fn set_scroll_position(&mut self, id: impl Into<Id>, position: impl Into<Vector2>) {
1097 self.context.set_scroll_position(id.into(), position.into());
1098 }
1099
1100 pub fn eval(&mut self) -> Vec<RenderCommand<CustomElementData>> {
1102 #[cfg(feature = "net")]
1104 net::NET_MANAGER.lock().unwrap().clean();
1105
1106 let commands = self.context.end_layout();
1107 let mut result = Vec::new();
1108 for cmd in commands {
1109 result.push(RenderCommand::from_engine_render_command(cmd));
1110 }
1111
1112 #[cfg(all(feature = "a11y", target_arch = "wasm32"))]
1114 {
1115 let accessibility_bounds = self.accessibility_bounds();
1116
1117 accessibility_web::sync_accessibility_tree(
1118 &mut self.web_a11y_state,
1119 &self.context.accessibility_configs,
1120 &accessibility_bounds,
1121 &self.context.accessibility_element_order,
1122 self.context.focused_element_id,
1123 self.context.layout_dimensions,
1124 );
1125 }
1126
1127 #[cfg(all(feature = "a11y", not(target_arch = "wasm32")))]
1129 {
1130 let accessibility_bounds = self.accessibility_bounds();
1131
1132 let a11y_actions = accessibility_native::sync_accessibility_tree(
1133 &mut self.native_a11y_state,
1134 &self.context.accessibility_configs,
1135 &accessibility_bounds,
1136 &self.context.accessibility_element_order,
1137 self.context.focused_element_id,
1138 self.context.layout_dimensions,
1139 );
1140 for action in a11y_actions {
1141 match action {
1142 accessibility_native::PendingA11yAction::Focus(target_id) => {
1143 self.context.change_focus(target_id);
1144 }
1145 accessibility_native::PendingA11yAction::Click(target_id) => {
1146 self.context.fire_press(target_id);
1147 }
1148 }
1149 }
1150 }
1151
1152 result
1153 }
1154
1155 pub async fn show(
1157 &mut self,
1158 handle_custom_command: impl Fn(&RenderCommand<CustomElementData>),
1159 ) {
1160 let commands = self.eval();
1161 renderer::render(commands, handle_custom_command).await;
1162 }
1163}
1164
1165#[cfg(target_arch = "wasm32")]
1166extern "C" {
1167 fn ply_show_virtual_keyboard(show: bool);
1168}
1169
1170#[cfg(test)]
1171mod tests {
1172 use super::*;
1173 use color::Color;
1174 use layout::{Padding, Sizing};
1175
1176 #[rustfmt::skip]
1177 #[test]
1178 fn test_begin() {
1179 let mut ply = Ply::<()>::new_headless(Dimensions::new(800.0, 600.0));
1180
1181 ply.set_measure_text_function(|_, _| {
1182 Dimensions::new(100.0, 24.0)
1183 });
1184
1185 let mut ui = ply.begin();
1186
1187 ui.element().width(fixed!(100.0)).height(fixed!(100.0))
1188 .background_color(0xFFFFFF)
1189 .children(|ui| {
1190 ui.element().width(fixed!(100.0)).height(fixed!(100.0))
1191 .background_color(0xFFFFFF)
1192 .children(|ui| {
1193 ui.element().width(fixed!(100.0)).height(fixed!(100.0))
1194 .background_color(0xFFFFFF)
1195 .children(|ui| {
1196 ui.text("test", |t| t
1197 .color(0xFFFFFF)
1198 .font_size(24)
1199 );
1200 });
1201 });
1202 });
1203
1204 ui.element()
1205 .border(|b| b
1206 .color(0xFFFF00)
1207 .all(2)
1208 )
1209 .corner_radius(10.0)
1210 .children(|ui| {
1211 ui.element().width(fixed!(50.0)).height(fixed!(50.0))
1212 .background_color(0x00FFFF)
1213 .empty();
1214 });
1215
1216 let items = ui.eval();
1217
1218 for item in &items {
1219 println!(
1220 "id: {}\nbbox: {:?}\nconfig: {:?}",
1221 item.id, item.bounding_box, item.config,
1222 );
1223 }
1224
1225 assert_eq!(items.len(), 6);
1226
1227 assert_eq!(items[0].bounding_box.x, 0.0);
1228 assert_eq!(items[0].bounding_box.y, 0.0);
1229 assert_eq!(items[0].bounding_box.width, 100.0);
1230 assert_eq!(items[0].bounding_box.height, 100.0);
1231 match &items[0].config {
1232 render_commands::RenderCommandConfig::Rectangle(rect) => {
1233 assert_eq!(rect.color.r, 255.0);
1234 assert_eq!(rect.color.g, 255.0);
1235 assert_eq!(rect.color.b, 255.0);
1236 assert_eq!(rect.color.a, 255.0);
1237 }
1238 _ => panic!("Expected Rectangle config for item 0"),
1239 }
1240
1241 assert_eq!(items[1].bounding_box.x, 0.0);
1242 assert_eq!(items[1].bounding_box.y, 0.0);
1243 assert_eq!(items[1].bounding_box.width, 100.0);
1244 assert_eq!(items[1].bounding_box.height, 100.0);
1245 match &items[1].config {
1246 render_commands::RenderCommandConfig::Rectangle(rect) => {
1247 assert_eq!(rect.color.r, 255.0);
1248 assert_eq!(rect.color.g, 255.0);
1249 assert_eq!(rect.color.b, 255.0);
1250 assert_eq!(rect.color.a, 255.0);
1251 }
1252 _ => panic!("Expected Rectangle config for item 1"),
1253 }
1254
1255 assert_eq!(items[2].bounding_box.x, 0.0);
1256 assert_eq!(items[2].bounding_box.y, 0.0);
1257 assert_eq!(items[2].bounding_box.width, 100.0);
1258 assert_eq!(items[2].bounding_box.height, 100.0);
1259 match &items[2].config {
1260 render_commands::RenderCommandConfig::Rectangle(rect) => {
1261 assert_eq!(rect.color.r, 255.0);
1262 assert_eq!(rect.color.g, 255.0);
1263 assert_eq!(rect.color.b, 255.0);
1264 assert_eq!(rect.color.a, 255.0);
1265 }
1266 _ => panic!("Expected Rectangle config for item 2"),
1267 }
1268
1269 assert_eq!(items[3].bounding_box.x, 0.0);
1270 assert_eq!(items[3].bounding_box.y, 0.0);
1271 assert_eq!(items[3].bounding_box.width, 100.0);
1272 assert_eq!(items[3].bounding_box.height, 24.0);
1273 match &items[3].config {
1274 render_commands::RenderCommandConfig::Text(text) => {
1275 assert_eq!(text.text, "test");
1276 assert_eq!(text.color.r, 255.0);
1277 assert_eq!(text.color.g, 255.0);
1278 assert_eq!(text.color.b, 255.0);
1279 assert_eq!(text.color.a, 255.0);
1280 assert_eq!(text.font_size, 24);
1281 }
1282 _ => panic!("Expected Text config for item 3"),
1283 }
1284
1285 assert_eq!(items[4].bounding_box.x, 100.0);
1286 assert_eq!(items[4].bounding_box.y, 0.0);
1287 assert_eq!(items[4].bounding_box.width, 50.0);
1288 assert_eq!(items[4].bounding_box.height, 50.0);
1289 match &items[4].config {
1290 render_commands::RenderCommandConfig::Rectangle(rect) => {
1291 assert_eq!(rect.color.r, 0.0);
1292 assert_eq!(rect.color.g, 255.0);
1293 assert_eq!(rect.color.b, 255.0);
1294 assert_eq!(rect.color.a, 255.0);
1295 }
1296 _ => panic!("Expected Rectangle config for item 4"),
1297 }
1298
1299 assert_eq!(items[5].bounding_box.x, 100.0);
1300 assert_eq!(items[5].bounding_box.y, 0.0);
1301 assert_eq!(items[5].bounding_box.width, 50.0);
1302 assert_eq!(items[5].bounding_box.height, 50.0);
1303 match &items[5].config {
1304 render_commands::RenderCommandConfig::Border(border) => {
1305 assert_eq!(border.color.r, 255.0);
1306 assert_eq!(border.color.g, 255.0);
1307 assert_eq!(border.color.b, 0.0);
1308 assert_eq!(border.color.a, 255.0);
1309 assert_eq!(border.corner_radii.top_left, 10.0);
1310 assert_eq!(border.corner_radii.top_right, 10.0);
1311 assert_eq!(border.corner_radii.bottom_left, 10.0);
1312 assert_eq!(border.corner_radii.bottom_right, 10.0);
1313 assert_eq!(border.width.left, 2);
1314 assert_eq!(border.width.right, 2);
1315 assert_eq!(border.width.top, 2);
1316 assert_eq!(border.width.bottom, 2);
1317 }
1318 _ => panic!("Expected Border config for item 5"),
1319 }
1320 }
1321
1322 #[rustfmt::skip]
1323 #[test]
1324 fn test_example() {
1325 let mut ply = Ply::<()>::new_headless(Dimensions::new(1000.0, 1000.0));
1326
1327 let mut ui = ply.begin();
1328
1329 ui.set_measure_text_function(|_, _| {
1330 Dimensions::new(100.0, 24.0)
1331 });
1332
1333 for &(label, level) in &[("Road", 1), ("Wall", 2), ("Tower", 3)] {
1334 ui.element().width(grow!()).height(fixed!(36.0))
1335 .layout(|l| l
1336 .direction(crate::layout::LayoutDirection::LeftToRight)
1337 .gap(12)
1338 .align(crate::align::AlignX::Left, crate::align::AlignY::CenterY)
1339 )
1340 .children(|ui| {
1341 ui.text(label, |t| t
1342 .font_size(18)
1343 .color(0xFFFFFF)
1344 );
1345 ui.element().width(grow!()).height(fixed!(18.0))
1346 .corner_radius(9.0)
1347 .background_color(0x555555)
1348 .children(|ui| {
1349 ui.element()
1350 .width(fixed!(300.0 * level as f32 / 3.0))
1351 .height(grow!())
1352 .corner_radius(9.0)
1353 .background_color(0x45A85A)
1354 .empty();
1355 });
1356 });
1357 }
1358
1359 let items = ui.eval();
1360
1361 for item in &items {
1362 println!(
1363 "id: {}\nbbox: {:?}\nconfig: {:?}",
1364 item.id, item.bounding_box, item.config,
1365 );
1366 }
1367
1368 assert_eq!(items.len(), 9);
1369
1370 assert_eq!(items[0].bounding_box.x, 0.0);
1372 assert_eq!(items[0].bounding_box.y, 6.0);
1373 assert_eq!(items[0].bounding_box.width, 100.0);
1374 assert_eq!(items[0].bounding_box.height, 24.0);
1375 match &items[0].config {
1376 render_commands::RenderCommandConfig::Text(text) => {
1377 assert_eq!(text.text, "Road");
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 0"),
1385 }
1386
1387 assert_eq!(items[1].bounding_box.x, 112.0);
1389 assert_eq!(items[1].bounding_box.y, 9.0);
1390 assert_eq!(items[1].bounding_box.width, 163.99142);
1391 assert_eq!(items[1].bounding_box.height, 18.0);
1392 match &items[1].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 1"),
1404 }
1405
1406 assert_eq!(items[2].bounding_box.x, 112.0);
1408 assert_eq!(items[2].bounding_box.y, 9.0);
1409 assert_eq!(items[2].bounding_box.width, 100.0);
1410 assert_eq!(items[2].bounding_box.height, 18.0);
1411 match &items[2].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 2"),
1423 }
1424
1425 assert_eq!(items[3].bounding_box.x, 275.99142);
1427 assert_eq!(items[3].bounding_box.y, 6.0);
1428 assert_eq!(items[3].bounding_box.width, 100.0);
1429 assert_eq!(items[3].bounding_box.height, 24.0);
1430 match &items[3].config {
1431 render_commands::RenderCommandConfig::Text(text) => {
1432 assert_eq!(text.text, "Wall");
1433 assert_eq!(text.color.r, 255.0);
1434 assert_eq!(text.color.g, 255.0);
1435 assert_eq!(text.color.b, 255.0);
1436 assert_eq!(text.color.a, 255.0);
1437 assert_eq!(text.font_size, 18);
1438 }
1439 _ => panic!("Expected Text config for item 3"),
1440 }
1441
1442 assert_eq!(items[4].bounding_box.x, 387.99142);
1444 assert_eq!(items[4].bounding_box.y, 9.0);
1445 assert_eq!(items[4].bounding_box.width, 200.0);
1446 assert_eq!(items[4].bounding_box.height, 18.0);
1447 match &items[4].config {
1448 render_commands::RenderCommandConfig::Rectangle(rect) => {
1449 assert_eq!(rect.color.r, 85.0);
1450 assert_eq!(rect.color.g, 85.0);
1451 assert_eq!(rect.color.b, 85.0);
1452 assert_eq!(rect.color.a, 255.0);
1453 assert_eq!(rect.corner_radii.top_left, 9.0);
1454 assert_eq!(rect.corner_radii.top_right, 9.0);
1455 assert_eq!(rect.corner_radii.bottom_left, 9.0);
1456 assert_eq!(rect.corner_radii.bottom_right, 9.0);
1457 }
1458 _ => panic!("Expected Rectangle config for item 4"),
1459 }
1460
1461 assert_eq!(items[5].bounding_box.x, 387.99142);
1463 assert_eq!(items[5].bounding_box.y, 9.0);
1464 assert_eq!(items[5].bounding_box.width, 200.0);
1465 assert_eq!(items[5].bounding_box.height, 18.0);
1466 match &items[5].config {
1467 render_commands::RenderCommandConfig::Rectangle(rect) => {
1468 assert_eq!(rect.color.r, 69.0);
1469 assert_eq!(rect.color.g, 168.0);
1470 assert_eq!(rect.color.b, 90.0);
1471 assert_eq!(rect.color.a, 255.0);
1472 assert_eq!(rect.corner_radii.top_left, 9.0);
1473 assert_eq!(rect.corner_radii.top_right, 9.0);
1474 assert_eq!(rect.corner_radii.bottom_left, 9.0);
1475 assert_eq!(rect.corner_radii.bottom_right, 9.0);
1476 }
1477 _ => panic!("Expected Rectangle config for item 5"),
1478 }
1479
1480 assert_eq!(items[6].bounding_box.x, 587.99146);
1482 assert_eq!(items[6].bounding_box.y, 6.0);
1483 assert_eq!(items[6].bounding_box.width, 100.0);
1484 assert_eq!(items[6].bounding_box.height, 24.0);
1485 match &items[6].config {
1486 render_commands::RenderCommandConfig::Text(text) => {
1487 assert_eq!(text.text, "Tower");
1488 assert_eq!(text.color.r, 255.0);
1489 assert_eq!(text.color.g, 255.0);
1490 assert_eq!(text.color.b, 255.0);
1491 assert_eq!(text.color.a, 255.0);
1492 assert_eq!(text.font_size, 18);
1493 }
1494 _ => panic!("Expected Text config for item 6"),
1495 }
1496
1497 assert_eq!(items[7].bounding_box.x, 699.99146);
1499 assert_eq!(items[7].bounding_box.y, 9.0);
1500 assert_eq!(items[7].bounding_box.width, 300.0);
1501 assert_eq!(items[7].bounding_box.height, 18.0);
1502 match &items[7].config {
1503 render_commands::RenderCommandConfig::Rectangle(rect) => {
1504 assert_eq!(rect.color.r, 85.0);
1505 assert_eq!(rect.color.g, 85.0);
1506 assert_eq!(rect.color.b, 85.0);
1507 assert_eq!(rect.color.a, 255.0);
1508 assert_eq!(rect.corner_radii.top_left, 9.0);
1509 assert_eq!(rect.corner_radii.top_right, 9.0);
1510 assert_eq!(rect.corner_radii.bottom_left, 9.0);
1511 assert_eq!(rect.corner_radii.bottom_right, 9.0);
1512 }
1513 _ => panic!("Expected Rectangle config for item 7"),
1514 }
1515
1516 assert_eq!(items[8].bounding_box.x, 699.99146);
1518 assert_eq!(items[8].bounding_box.y, 9.0);
1519 assert_eq!(items[8].bounding_box.width, 300.0);
1520 assert_eq!(items[8].bounding_box.height, 18.0);
1521 match &items[8].config {
1522 render_commands::RenderCommandConfig::Rectangle(rect) => {
1523 assert_eq!(rect.color.r, 69.0);
1524 assert_eq!(rect.color.g, 168.0);
1525 assert_eq!(rect.color.b, 90.0);
1526 assert_eq!(rect.color.a, 255.0);
1527 assert_eq!(rect.corner_radii.top_left, 9.0);
1528 assert_eq!(rect.corner_radii.top_right, 9.0);
1529 assert_eq!(rect.corner_radii.bottom_left, 9.0);
1530 assert_eq!(rect.corner_radii.bottom_right, 9.0);
1531 }
1532 _ => panic!("Expected Rectangle config for item 8"),
1533 }
1534 }
1535
1536 #[test]
1537 fn test_grow_weights_distribute_proportionally() {
1538 let mut ply = Ply::<()>::new_headless(Dimensions::new(600.0, 120.0));
1539 let mut ui = ply.begin();
1540
1541 ui.element()
1542 .width(fixed!(600.0))
1543 .height(fixed!(120.0))
1544 .layout(|l| l.direction(crate::layout::LayoutDirection::LeftToRight))
1545 .children(|ui| {
1546 ui.element()
1547 .width(grow!(0.0, f32::MAX, 1.0))
1548 .height(grow!())
1549 .background_color(0xFF0000)
1550 .empty();
1551
1552 ui.element()
1553 .width(grow!(0.0, f32::MAX, 2.0))
1554 .height(grow!())
1555 .background_color(0x00FF00)
1556 .empty();
1557 });
1558
1559 let items = ui.eval();
1560 assert_eq!(items.len(), 2);
1561 assert_eq!(items[0].bounding_box.width, 200.0);
1562 assert_eq!(items[1].bounding_box.width, 400.0);
1563 }
1564
1565 #[test]
1566 fn test_zero_weight_grow_behaves_like_fit() {
1567 let mut ply = Ply::<()>::new_headless(Dimensions::new(600.0, 120.0));
1568 let mut ui = ply.begin();
1569
1570 ui.element()
1571 .width(fixed!(600.0))
1572 .height(fixed!(120.0))
1573 .layout(|l| l.direction(crate::layout::LayoutDirection::LeftToRight))
1574 .children(|ui| {
1575 ui.element()
1576 .width(grow!(50.0, f32::MAX, 0.0))
1577 .height(grow!())
1578 .background_color(0xFF0000)
1579 .empty();
1580
1581 ui.element()
1582 .width(grow!())
1583 .height(grow!())
1584 .background_color(0x00FF00)
1585 .empty();
1586 });
1587
1588 let items = ui.eval();
1589 assert_eq!(items.len(), 2);
1590 assert_eq!(items[0].bounding_box.width, 50.0);
1591 assert_eq!(items[1].bounding_box.width, 550.0);
1592 }
1593
1594 #[test]
1595 fn test_single_main_axis_grow_respects_max() {
1596 let mut ply = Ply::<()>::new_headless(Dimensions::new(600.0, 120.0));
1597 let mut ui = ply.begin();
1598
1599 ui.element()
1600 .width(fixed!(600.0))
1601 .height(fixed!(120.0))
1602 .layout(|l| l.direction(crate::layout::LayoutDirection::LeftToRight))
1603 .children(|ui| {
1604 ui.element()
1605 .width(grow!(0.0, 300.0, 2.0))
1606 .height(grow!())
1607 .background_color(0xFF0000)
1608 .empty();
1609 });
1610
1611 let items = ui.eval();
1612 assert_eq!(items.len(), 1);
1613 assert_eq!(items[0].bounding_box.width, 300.0);
1614 }
1615
1616 #[rustfmt::skip]
1617 #[test]
1618 fn test_floating() {
1619 let mut ply = Ply::<()>::new_headless(Dimensions::new(1000.0, 1000.0));
1620
1621 let mut ui = ply.begin();
1622
1623 ui.set_measure_text_function(|_, _| {
1624 Dimensions::new(100.0, 24.0)
1625 });
1626
1627 ui.element().width(fixed!(20.0)).height(fixed!(20.0))
1628 .layout(|l| l.align(crate::align::AlignX::CenterX, crate::align::AlignY::CenterY))
1629 .floating(|f| f
1630 .attach_root()
1631 .anchor((crate::align::AlignX::CenterX, crate::align::AlignY::CenterY), (crate::align::AlignX::Left, crate::align::AlignY::Top))
1632 .offset((100.0, 150.0))
1633 .passthrough()
1634 .z_index(110)
1635 )
1636 .corner_radius(10.0)
1637 .background_color(0x4488DD)
1638 .children(|ui| {
1639 ui.text("Re", |t| t
1640 .font_size(6)
1641 .color(0xFFFFFF)
1642 );
1643 });
1644
1645 let items = ui.eval();
1646
1647 for item in &items {
1648 println!(
1649 "id: {}\nbbox: {:?}\nconfig: {:?}",
1650 item.id, item.bounding_box, item.config,
1651 );
1652 }
1653
1654 assert_eq!(items.len(), 2);
1655
1656 assert_eq!(items[0].bounding_box.x, 90.0);
1657 assert_eq!(items[0].bounding_box.y, 140.0);
1658 assert_eq!(items[0].bounding_box.width, 20.0);
1659 assert_eq!(items[0].bounding_box.height, 20.0);
1660 match &items[0].config {
1661 render_commands::RenderCommandConfig::Rectangle(rect) => {
1662 assert_eq!(rect.color.r, 68.0);
1663 assert_eq!(rect.color.g, 136.0);
1664 assert_eq!(rect.color.b, 221.0);
1665 assert_eq!(rect.color.a, 255.0);
1666 assert_eq!(rect.corner_radii.top_left, 10.0);
1667 assert_eq!(rect.corner_radii.top_right, 10.0);
1668 assert_eq!(rect.corner_radii.bottom_left, 10.0);
1669 assert_eq!(rect.corner_radii.bottom_right, 10.0);
1670 }
1671 _ => panic!("Expected Rectangle config for item 0"),
1672 }
1673
1674 assert_eq!(items[1].bounding_box.x, 50.0);
1675 assert_eq!(items[1].bounding_box.y, 138.0);
1676 assert_eq!(items[1].bounding_box.width, 100.0);
1677 assert_eq!(items[1].bounding_box.height, 24.0);
1678 match &items[1].config {
1679 render_commands::RenderCommandConfig::Text(text) => {
1680 assert_eq!(text.text, "Re");
1681 assert_eq!(text.color.r, 255.0);
1682 assert_eq!(text.color.g, 255.0);
1683 assert_eq!(text.color.b, 255.0);
1684 assert_eq!(text.color.a, 255.0);
1685 assert_eq!(text.font_size, 6);
1686 }
1687 _ => panic!("Expected Text config for item 1"),
1688 }
1689 }
1690
1691 #[rustfmt::skip]
1692 #[test]
1693 fn test_simple_text_measure() {
1694 let mut ply = Ply::<()>::new_headless(Dimensions::new(800.0, 600.0));
1695
1696 ply.set_measure_text_function(|_text, _config| {
1697 Dimensions::default()
1698 });
1699
1700 let mut ui = ply.begin();
1701
1702 ui.element()
1703 .id("parent_rect")
1704 .width(Sizing::Fixed(100.0))
1705 .height(Sizing::Fixed(100.0))
1706 .layout(|l| l
1707 .padding(Padding::all(10))
1708 )
1709 .background_color(Color::rgb(255., 255., 255.))
1710 .children(|ui| {
1711 ui.text(&format!("{}", 1234), |t| t
1712 .color(Color::rgb(255., 255., 255.))
1713 .font_size(24)
1714 );
1715 });
1716
1717 let _items = ui.eval();
1718 }
1719
1720 #[rustfmt::skip]
1721 #[test]
1722 fn test_shader_begin_end() {
1723 use shaders::ShaderAsset;
1724
1725 let test_shader = ShaderAsset::Source {
1726 file_name: "test_effect.glsl",
1727 fragment: "#version 100\nprecision lowp float;\nvarying vec2 uv;\nuniform sampler2D Texture;\nvoid main() { gl_FragColor = texture2D(Texture, uv); }",
1728 };
1729
1730 let mut ply = Ply::<()>::new_headless(Dimensions::new(800.0, 600.0));
1731 ply.set_measure_text_function(|_, _| Dimensions::new(100.0, 24.0));
1732
1733 let mut ui = ply.begin();
1734
1735 ui.element()
1737 .width(fixed!(200.0)).height(fixed!(200.0))
1738 .background_color(0xFF0000)
1739 .shader(&test_shader, |s| {
1740 s.uniform("time", 1.0f32);
1741 })
1742 .children(|ui| {
1743 ui.element()
1744 .width(fixed!(100.0)).height(fixed!(100.0))
1745 .background_color(0x00FF00)
1746 .empty();
1747 });
1748
1749 let items = ui.eval();
1750
1751 for (i, item) in items.iter().enumerate() {
1752 println!(
1753 "[{}] config: {:?}, bbox: {:?}",
1754 i, item.config, item.bounding_box,
1755 );
1756 }
1757
1758 assert!(items.len() >= 4, "Expected at least 4 items, got {}", items.len());
1764
1765 match &items[0].config {
1766 render_commands::RenderCommandConfig::GroupBegin { shader, visual_rotation } => {
1767 let config = shader.as_ref().expect("GroupBegin should have shader config");
1768 assert!(!config.fragment.is_empty(), "GroupBegin should have fragment source");
1769 assert_eq!(config.uniforms.len(), 1);
1770 assert_eq!(config.uniforms[0].name, "time");
1771 assert!(visual_rotation.is_none(), "Shader-only group should have no visual_rotation");
1772 }
1773 other => panic!("Expected GroupBegin for item 0, got {:?}", other),
1774 }
1775
1776 match &items[1].config {
1777 render_commands::RenderCommandConfig::Rectangle(rect) => {
1778 assert_eq!(rect.color.r, 255.0);
1779 assert_eq!(rect.color.g, 0.0);
1780 assert_eq!(rect.color.b, 0.0);
1781 }
1782 other => panic!("Expected Rectangle for item 1, got {:?}", other),
1783 }
1784
1785 match &items[2].config {
1786 render_commands::RenderCommandConfig::Rectangle(rect) => {
1787 assert_eq!(rect.color.r, 0.0);
1788 assert_eq!(rect.color.g, 255.0);
1789 assert_eq!(rect.color.b, 0.0);
1790 }
1791 other => panic!("Expected Rectangle for item 2, got {:?}", other),
1792 }
1793
1794 match &items[3].config {
1795 render_commands::RenderCommandConfig::GroupEnd => {}
1796 other => panic!("Expected GroupEnd for item 3, got {:?}", other),
1797 }
1798 }
1799
1800 #[rustfmt::skip]
1801 #[test]
1802 fn test_multiple_shaders_nested() {
1803 use shaders::ShaderAsset;
1804
1805 let shader_a = ShaderAsset::Source {
1806 file_name: "shader_a.glsl",
1807 fragment: "#version 100\nprecision lowp float;\nvoid main() { gl_FragColor = vec4(1.0); }",
1808 };
1809 let shader_b = ShaderAsset::Source {
1810 file_name: "shader_b.glsl",
1811 fragment: "#version 100\nprecision lowp float;\nvoid main() { gl_FragColor = vec4(0.5); }",
1812 };
1813
1814 let mut ply = Ply::<()>::new_headless(Dimensions::new(800.0, 600.0));
1815 ply.set_measure_text_function(|_, _| Dimensions::new(100.0, 24.0));
1816
1817 let mut ui = ply.begin();
1818
1819 ui.element()
1821 .width(fixed!(200.0)).height(fixed!(200.0))
1822 .background_color(0xFFFFFF)
1823 .shader(&shader_a, |s| { s.uniform("val", 1.0f32); })
1824 .shader(&shader_b, |s| { s.uniform("val", 2.0f32); })
1825 .children(|ui| {
1826 ui.element()
1827 .width(fixed!(50.0)).height(fixed!(50.0))
1828 .background_color(0x0000FF)
1829 .empty();
1830 });
1831
1832 let items = ui.eval();
1833
1834 for (i, item) in items.iter().enumerate() {
1835 println!("[{}] config: {:?}", i, item.config);
1836 }
1837
1838 assert!(items.len() >= 6, "Expected at least 6 items, got {}", items.len());
1846
1847 match &items[0].config {
1848 render_commands::RenderCommandConfig::GroupBegin { shader, .. } => {
1849 let config = shader.as_ref().unwrap();
1850 assert!(config.fragment.contains("0.5"), "Expected shader_b fragment");
1852 }
1853 other => panic!("Expected GroupBegin(shader_b) for item 0, got {:?}", other),
1854 }
1855 match &items[1].config {
1856 render_commands::RenderCommandConfig::GroupBegin { shader, .. } => {
1857 let config = shader.as_ref().unwrap();
1858 assert!(config.fragment.contains("1.0"), "Expected shader_a fragment");
1860 }
1861 other => panic!("Expected GroupBegin(shader_a) for item 1, got {:?}", other),
1862 }
1863 match &items[2].config {
1864 render_commands::RenderCommandConfig::Rectangle(_) => {}
1865 other => panic!("Expected Rectangle for item 2, got {:?}", other),
1866 }
1867 match &items[3].config {
1868 render_commands::RenderCommandConfig::Rectangle(_) => {}
1869 other => panic!("Expected Rectangle for item 3, got {:?}", other),
1870 }
1871 match &items[4].config {
1872 render_commands::RenderCommandConfig::GroupEnd => {}
1873 other => panic!("Expected GroupEnd for item 4, got {:?}", other),
1874 }
1875 match &items[5].config {
1876 render_commands::RenderCommandConfig::GroupEnd => {}
1877 other => panic!("Expected GroupEnd for item 5, got {:?}", other),
1878 }
1879 }
1880
1881 #[rustfmt::skip]
1882 #[test]
1883 fn test_effect_on_render_command() {
1884 use shaders::ShaderAsset;
1885
1886 let effect_shader = ShaderAsset::Source {
1887 file_name: "gradient.glsl",
1888 fragment: "#version 100\nprecision lowp float;\nvoid main() { gl_FragColor = vec4(1.0); }",
1889 };
1890
1891 let mut ply = Ply::<()>::new_headless(Dimensions::new(800.0, 600.0));
1892
1893 let mut ui = ply.begin();
1894
1895 ui.element()
1896 .width(fixed!(200.0)).height(fixed!(100.0))
1897 .background_color(0xFF0000)
1898 .effect(&effect_shader, |s| {
1899 s.uniform("color_a", [1.0f32, 0.0, 0.0, 1.0])
1900 .uniform("color_b", [0.0f32, 0.0, 1.0, 1.0]);
1901 })
1902 .empty();
1903
1904 let items = ui.eval();
1905
1906 assert_eq!(items.len(), 1, "Expected 1 item, got {}", items.len());
1907 assert_eq!(items[0].effects.len(), 1, "Expected 1 effect");
1908 assert_eq!(items[0].effects[0].uniforms.len(), 2);
1909 assert_eq!(items[0].effects[0].uniforms[0].name, "color_a");
1910 assert_eq!(items[0].effects[0].uniforms[1].name, "color_b");
1911 }
1912
1913 #[rustfmt::skip]
1914 #[test]
1915 fn test_visual_rotation_emits_group() {
1916 let mut ply = Ply::<()>::new_headless(Dimensions::new(800.0, 600.0));
1917 let mut ui = ply.begin();
1918
1919 ui.element()
1920 .width(fixed!(100.0)).height(fixed!(50.0))
1921 .background_color(0xFF0000)
1922 .rotate_visual(|r| r.degrees(45.0))
1923 .empty();
1924
1925 let items = ui.eval();
1926
1927 assert_eq!(items.len(), 3, "Expected 3 items, got {}", items.len());
1929
1930 match &items[0].config {
1931 render_commands::RenderCommandConfig::GroupBegin { shader, visual_rotation } => {
1932 assert!(shader.is_none(), "Rotation-only group should have no shader");
1933 let vr = visual_rotation.as_ref().expect("Should have visual_rotation");
1934 assert!((vr.rotation_radians - 45.0_f32.to_radians()).abs() < 0.001);
1935 assert_eq!(vr.pivot_x, 0.5);
1936 assert_eq!(vr.pivot_y, 0.5);
1937 assert!(!vr.flip_x);
1938 assert!(!vr.flip_y);
1939 }
1940 other => panic!("Expected GroupBegin for item 0, got {:?}", other),
1941 }
1942
1943 match &items[1].config {
1944 render_commands::RenderCommandConfig::Rectangle(_) => {}
1945 other => panic!("Expected Rectangle for item 1, got {:?}", other),
1946 }
1947
1948 match &items[2].config {
1949 render_commands::RenderCommandConfig::GroupEnd => {}
1950 other => panic!("Expected GroupEnd for item 2, got {:?}", other),
1951 }
1952 }
1953
1954 #[rustfmt::skip]
1955 #[test]
1956 fn test_visual_rotation_with_shader_merged() {
1957 use shaders::ShaderAsset;
1958
1959 let test_shader = ShaderAsset::Source {
1960 file_name: "merge_test.glsl",
1961 fragment: "#version 100\nprecision lowp float;\nvoid main() { gl_FragColor = vec4(1.0); }",
1962 };
1963
1964 let mut ply = Ply::<()>::new_headless(Dimensions::new(800.0, 600.0));
1965 let mut ui = ply.begin();
1966
1967 ui.element()
1969 .width(fixed!(100.0)).height(fixed!(100.0))
1970 .background_color(0xFF0000)
1971 .shader(&test_shader, |s| { s.uniform("v", 1.0f32); })
1972 .rotate_visual(|r| r.degrees(30.0).pivot((0.0, 0.0)))
1973 .empty();
1974
1975 let items = ui.eval();
1976
1977 assert_eq!(items.len(), 3, "Expected 3 items (merged), got {}", items.len());
1979
1980 match &items[0].config {
1981 render_commands::RenderCommandConfig::GroupBegin { shader, visual_rotation } => {
1982 assert!(shader.is_some(), "Merged group should have shader");
1983 let vr = visual_rotation.as_ref().expect("Merged group should have visual_rotation");
1984 assert!((vr.rotation_radians - 30.0_f32.to_radians()).abs() < 0.001);
1985 assert_eq!(vr.pivot_x, 0.0);
1986 assert_eq!(vr.pivot_y, 0.0);
1987 }
1988 other => panic!("Expected GroupBegin for item 0, got {:?}", other),
1989 }
1990 }
1991
1992 #[rustfmt::skip]
1993 #[test]
1994 fn test_visual_rotation_with_multiple_shaders() {
1995 use shaders::ShaderAsset;
1996
1997 let shader_a = ShaderAsset::Source {
1998 file_name: "vr_a.glsl",
1999 fragment: "#version 100\nprecision lowp float;\nvoid main() { gl_FragColor = vec4(1.0); }",
2000 };
2001 let shader_b = ShaderAsset::Source {
2002 file_name: "vr_b.glsl",
2003 fragment: "#version 100\nprecision lowp float;\nvoid main() { gl_FragColor = vec4(0.5); }",
2004 };
2005
2006 let mut ply = Ply::<()>::new_headless(Dimensions::new(800.0, 600.0));
2007 let mut ui = ply.begin();
2008
2009 ui.element()
2010 .width(fixed!(100.0)).height(fixed!(100.0))
2011 .background_color(0xFF0000)
2012 .shader(&shader_a, |s| { s.uniform("v", 1.0f32); })
2013 .shader(&shader_b, |s| { s.uniform("v", 2.0f32); })
2014 .rotate_visual(|r| r.degrees(90.0))
2015 .empty();
2016
2017 let items = ui.eval();
2018
2019 assert!(items.len() >= 5, "Expected at least 5 items, got {}", items.len());
2021
2022 match &items[0].config {
2024 render_commands::RenderCommandConfig::GroupBegin { shader, visual_rotation } => {
2025 assert!(shader.is_some(), "Outermost should have shader");
2026 assert!(visual_rotation.is_some(), "Outermost should have visual_rotation");
2027 }
2028 other => panic!("Expected GroupBegin for item 0, got {:?}", other),
2029 }
2030
2031 match &items[1].config {
2033 render_commands::RenderCommandConfig::GroupBegin { shader, visual_rotation } => {
2034 assert!(shader.is_some(), "Inner should have shader");
2035 assert!(visual_rotation.is_none(), "Inner should NOT have visual_rotation");
2036 }
2037 other => panic!("Expected GroupBegin for item 1, got {:?}", other),
2038 }
2039 }
2040
2041 #[rustfmt::skip]
2042 #[test]
2043 fn test_visual_rotation_noop_skipped() {
2044 let mut ply = Ply::<()>::new_headless(Dimensions::new(800.0, 600.0));
2045 let mut ui = ply.begin();
2046
2047 ui.element()
2049 .width(fixed!(100.0)).height(fixed!(100.0))
2050 .background_color(0xFF0000)
2051 .rotate_visual(|r| r.degrees(0.0))
2052 .empty();
2053
2054 let items = ui.eval();
2055
2056 assert_eq!(items.len(), 1, "Noop rotation should produce 1 item, got {}", items.len());
2058 match &items[0].config {
2059 render_commands::RenderCommandConfig::Rectangle(_) => {}
2060 other => panic!("Expected Rectangle, got {:?}", other),
2061 }
2062 }
2063
2064 #[rustfmt::skip]
2065 #[test]
2066 fn test_visual_rotation_flip_only() {
2067 let mut ply = Ply::<()>::new_headless(Dimensions::new(800.0, 600.0));
2068 let mut ui = ply.begin();
2069
2070 ui.element()
2072 .width(fixed!(100.0)).height(fixed!(100.0))
2073 .background_color(0xFF0000)
2074 .rotate_visual(|r| r.flip_x())
2075 .empty();
2076
2077 let items = ui.eval();
2078
2079 assert_eq!(items.len(), 3, "Flip-only should produce 3 items, got {}", items.len());
2081 match &items[0].config {
2082 render_commands::RenderCommandConfig::GroupBegin { visual_rotation, .. } => {
2083 let vr = visual_rotation.as_ref().expect("Should have rotation config");
2084 assert!(vr.flip_x);
2085 assert!(!vr.flip_y);
2086 assert_eq!(vr.rotation_radians, 0.0);
2087 }
2088 other => panic!("Expected GroupBegin, got {:?}", other),
2089 }
2090 }
2091
2092 #[rustfmt::skip]
2093 #[test]
2094 fn test_visual_rotation_preserves_bounding_box() {
2095 let mut ply = Ply::<()>::new_headless(Dimensions::new(800.0, 600.0));
2096 let mut ui = ply.begin();
2097
2098 ui.element()
2099 .width(fixed!(200.0)).height(fixed!(100.0))
2100 .background_color(0xFF0000)
2101 .rotate_visual(|r| r.degrees(45.0))
2102 .empty();
2103
2104 let items = ui.eval();
2105
2106 let rect = &items[1]; assert_eq!(rect.bounding_box.width, 200.0);
2109 assert_eq!(rect.bounding_box.height, 100.0);
2110 }
2111
2112 #[rustfmt::skip]
2113 #[test]
2114 fn test_visual_rotation_config_values() {
2115 let mut ply = Ply::<()>::new_headless(Dimensions::new(800.0, 600.0));
2116 let mut ui = ply.begin();
2117
2118 ui.element()
2119 .width(fixed!(100.0)).height(fixed!(100.0))
2120 .background_color(0xFF0000)
2121 .rotate_visual(|r| r
2122 .radians(std::f32::consts::FRAC_PI_2)
2123 .pivot((0.25, 0.75))
2124 .flip_x()
2125 .flip_y()
2126 )
2127 .empty();
2128
2129 let items = ui.eval();
2130
2131 match &items[0].config {
2132 render_commands::RenderCommandConfig::GroupBegin { visual_rotation, .. } => {
2133 let vr = visual_rotation.as_ref().unwrap();
2134 assert!((vr.rotation_radians - std::f32::consts::FRAC_PI_2).abs() < 0.001);
2135 assert_eq!(vr.pivot_x, 0.25);
2136 assert_eq!(vr.pivot_y, 0.75);
2137 assert!(vr.flip_x);
2138 assert!(vr.flip_y);
2139 }
2140 other => panic!("Expected GroupBegin, got {:?}", other),
2141 }
2142 }
2143
2144 #[rustfmt::skip]
2145 #[test]
2146 fn test_shape_rotation_emits_with_rotation() {
2147 let mut ply = Ply::<()>::new_headless(Dimensions::new(800.0, 600.0));
2148 let mut ui = ply.begin();
2149
2150 ui.element()
2151 .width(fixed!(100.0)).height(fixed!(50.0))
2152 .background_color(0xFF0000)
2153 .rotate_shape(|r| r.degrees(45.0))
2154 .empty();
2155
2156 let items = ui.eval();
2157
2158 assert_eq!(items.len(), 1, "Expected 1 item, got {}", items.len());
2160 let sr = items[0].shape_rotation.as_ref().expect("Should have shape_rotation");
2161 assert!((sr.rotation_radians - 45.0_f32.to_radians()).abs() < 0.001);
2162 assert!(!sr.flip_x);
2163 assert!(!sr.flip_y);
2164 }
2165
2166 #[rustfmt::skip]
2167 #[test]
2168 fn test_shape_rotation_aabb_90_degrees() {
2169 let mut ply = Ply::<()>::new_headless(Dimensions::new(800.0, 600.0));
2171 let mut ui = ply.begin();
2172
2173 ui.element().width(grow!()).height(grow!())
2174 .layout(|l| l)
2175 .children(|ui| {
2176 ui.element()
2177 .width(fixed!(200.0)).height(fixed!(100.0))
2178 .background_color(0xFF0000)
2179 .rotate_shape(|r| r.degrees(90.0))
2180 .empty();
2181 });
2182
2183 let items = ui.eval();
2184
2185 let rect = items.iter().find(|i| matches!(i.config, render_commands::RenderCommandConfig::Rectangle(_))).unwrap();
2187 assert!((rect.bounding_box.width - 200.0).abs() < 0.1, "width should be 200, got {}", rect.bounding_box.width);
2189 assert!((rect.bounding_box.height - 100.0).abs() < 0.1, "height should be 100, got {}", rect.bounding_box.height);
2190 }
2191
2192 #[rustfmt::skip]
2193 #[test]
2194 fn test_shape_rotation_aabb_45_degrees_sharp() {
2195 let mut ply = Ply::<()>::new_headless(Dimensions::new(800.0, 600.0));
2197 let mut ui = ply.begin();
2198
2199 ui.element().width(grow!()).height(grow!())
2201 .layout(|l| l.direction(layout::LayoutDirection::LeftToRight))
2202 .children(|ui| {
2203 ui.element()
2204 .width(fixed!(100.0)).height(fixed!(100.0))
2205 .background_color(0xFF0000)
2206 .rotate_shape(|r| r.degrees(45.0))
2207 .empty();
2208
2209 ui.element()
2211 .width(fixed!(50.0)).height(fixed!(50.0))
2212 .background_color(0x00FF00)
2213 .empty();
2214 });
2215
2216 let items = ui.eval();
2217
2218 let rects: Vec<_> = items.iter()
2220 .filter(|i| matches!(i.config, render_commands::RenderCommandConfig::Rectangle(_)))
2221 .collect();
2222 assert!(rects.len() >= 2, "Expected at least 2 rectangles, got {}", rects.len());
2223
2224 let expected_aabb_w = (2.0_f32.sqrt()) * 100.0; let green_x = rects[1].bounding_box.x;
2226 assert!((green_x - expected_aabb_w).abs() < 1.0,
2228 "Green rect x should be ~{}, got {}", expected_aabb_w, green_x);
2229 }
2230
2231 #[rustfmt::skip]
2232 #[test]
2233 fn test_shape_rotation_aabb_45_degrees_rounded() {
2234 let mut ply = Ply::<()>::new_headless(Dimensions::new(800.0, 600.0));
2237 let mut ui = ply.begin();
2238
2239 ui.element().width(grow!()).height(grow!())
2240 .layout(|l| l.direction(layout::LayoutDirection::LeftToRight))
2241 .children(|ui| {
2242 ui.element()
2243 .width(fixed!(100.0)).height(fixed!(100.0))
2244 .corner_radius(10.0)
2245 .background_color(0xFF0000)
2246 .rotate_shape(|r| r.degrees(45.0))
2247 .empty();
2248
2249 ui.element()
2250 .width(fixed!(50.0)).height(fixed!(50.0))
2251 .background_color(0x00FF00)
2252 .empty();
2253 });
2254
2255 let items = ui.eval();
2256
2257 let rects: Vec<_> = items.iter()
2258 .filter(|i| matches!(i.config, render_commands::RenderCommandConfig::Rectangle(_)))
2259 .collect();
2260 assert!(rects.len() >= 2);
2261
2262 let expected_aabb_w = 80.0 * 2.0_f32.sqrt() + 20.0;
2264 let green_x = rects[1].bounding_box.x;
2265 assert!((green_x - expected_aabb_w).abs() < 1.0,
2267 "Green rect x should be ~{}, got {}", expected_aabb_w, green_x);
2268 }
2269
2270 #[rustfmt::skip]
2271 #[test]
2272 fn test_shape_rotation_noop_no_aabb_change() {
2273 let mut ply = Ply::<()>::new_headless(Dimensions::new(800.0, 600.0));
2275 let mut ui = ply.begin();
2276
2277 ui.element()
2278 .width(fixed!(100.0)).height(fixed!(50.0))
2279 .background_color(0xFF0000)
2280 .rotate_shape(|r| r.degrees(0.0))
2281 .empty();
2282
2283 let items = ui.eval();
2284 assert_eq!(items.len(), 1);
2285 assert_eq!(items[0].bounding_box.width, 100.0);
2286 assert_eq!(items[0].bounding_box.height, 50.0);
2287 assert!(items[0].shape_rotation.is_none(), "Noop shape rotation should be filtered");
2290 }
2291
2292 #[rustfmt::skip]
2293 #[test]
2294 fn test_shape_rotation_flip_only() {
2295 let mut ply = Ply::<()>::new_headless(Dimensions::new(800.0, 600.0));
2297 let mut ui = ply.begin();
2298
2299 ui.element()
2300 .width(fixed!(100.0)).height(fixed!(50.0))
2301 .background_color(0xFF0000)
2302 .rotate_shape(|r| r.flip_x())
2303 .empty();
2304
2305 let items = ui.eval();
2306 assert_eq!(items.len(), 1);
2307 let sr = items[0].shape_rotation.as_ref().expect("flip_x should produce shape_rotation");
2308 assert!(sr.flip_x);
2309 assert!(!sr.flip_y);
2310 assert_eq!(items[0].bounding_box.width, 100.0);
2312 assert_eq!(items[0].bounding_box.height, 50.0);
2313 }
2314
2315 #[rustfmt::skip]
2316 #[test]
2317 fn test_shape_rotation_180_no_aabb_change() {
2318 let mut ply = Ply::<()>::new_headless(Dimensions::new(800.0, 600.0));
2320 let mut ui = ply.begin();
2321
2322 ui.element()
2323 .width(fixed!(200.0)).height(fixed!(100.0))
2324 .background_color(0xFF0000)
2325 .rotate_shape(|r| r.degrees(180.0))
2326 .empty();
2327
2328 let items = ui.eval();
2329 assert_eq!(items.len(), 1);
2330 assert_eq!(items[0].bounding_box.width, 200.0);
2331 assert_eq!(items[0].bounding_box.height, 100.0);
2332 }
2333
2334 #[test]
2335 fn test_classify_angle() {
2336 use math::{classify_angle, AngleType};
2337 assert_eq!(classify_angle(0.0), AngleType::Zero);
2338 assert_eq!(classify_angle(std::f32::consts::TAU), AngleType::Zero);
2339 assert_eq!(classify_angle(-std::f32::consts::TAU), AngleType::Zero);
2340 assert_eq!(classify_angle(std::f32::consts::FRAC_PI_2), AngleType::Right90);
2341 assert_eq!(classify_angle(std::f32::consts::PI), AngleType::Straight180);
2342 assert_eq!(classify_angle(3.0 * std::f32::consts::FRAC_PI_2), AngleType::Right270);
2343 match classify_angle(1.0) {
2344 AngleType::Arbitrary(v) => assert!((v - 1.0).abs() < 0.01),
2345 other => panic!("Expected Arbitrary, got {:?}", other),
2346 }
2347 }
2348
2349 #[test]
2350 fn test_compute_rotated_aabb_zero() {
2351 use math::compute_rotated_aabb;
2352 use layout::CornerRadius;
2353 let cr = CornerRadius::default();
2354 let (w, h) = compute_rotated_aabb(100.0, 50.0, &cr, 0.0);
2355 assert_eq!(w, 100.0);
2356 assert_eq!(h, 50.0);
2357 }
2358
2359 #[test]
2360 fn test_compute_rotated_aabb_90() {
2361 use math::compute_rotated_aabb;
2362 use layout::CornerRadius;
2363 let cr = CornerRadius::default();
2364 let (w, h) = compute_rotated_aabb(200.0, 100.0, &cr, std::f32::consts::FRAC_PI_2);
2365 assert!((w - 100.0).abs() < 0.1, "w should be 100, got {}", w);
2366 assert!((h - 200.0).abs() < 0.1, "h should be 200, got {}", h);
2367 }
2368
2369 #[test]
2370 fn test_compute_rotated_aabb_45_sharp() {
2371 use math::compute_rotated_aabb;
2372 use layout::CornerRadius;
2373 let cr = CornerRadius::default();
2374 let theta = std::f32::consts::FRAC_PI_4;
2375 let (w, h) = compute_rotated_aabb(100.0, 100.0, &cr, theta);
2376 let expected = 100.0 * 2.0_f32.sqrt();
2377 assert!((w - expected).abs() < 0.5, "w should be ~{}, got {}", expected, w);
2378 assert!((h - expected).abs() < 0.5, "h should be ~{}, got {}", expected, h);
2379 }
2380
2381 #[test]
2382 fn test_compute_rotated_aabb_45_rounded() {
2383 use math::compute_rotated_aabb;
2384 use layout::CornerRadius;
2385 let cr = CornerRadius { top_left: 10.0, top_right: 10.0, bottom_left: 10.0, bottom_right: 10.0 };
2386 let theta = std::f32::consts::FRAC_PI_4;
2387 let (w, h) = compute_rotated_aabb(100.0, 100.0, &cr, theta);
2388 let expected = 80.0 * 2.0_f32.sqrt() + 20.0; assert!((w - expected).abs() < 0.5, "w should be ~{}, got {}", expected, w);
2390 assert!((h - expected).abs() < 0.5, "h should be ~{}, got {}", expected, h);
2391 }
2392
2393 #[test]
2394 fn test_on_press_callback_fires() {
2395 use std::cell::RefCell;
2396 use std::rc::Rc;
2397
2398 let mut ply = Ply::<()>::new_headless(Dimensions::new(800.0, 600.0));
2399 let press_count = Rc::new(RefCell::new(0u32));
2400 let release_count = Rc::new(RefCell::new(0u32));
2401
2402 {
2404 let mut ui = ply.begin();
2405 ui.element()
2406 .id("btn")
2407 .width(fixed!(100.0))
2408 .height(fixed!(100.0))
2409 .empty();
2410 ui.eval();
2411 }
2412
2413 {
2415 let pc = press_count.clone();
2416 let rc = release_count.clone();
2417 let mut ui = ply.begin();
2418 ui.element()
2419 .id("btn")
2420 .width(fixed!(100.0))
2421 .height(fixed!(100.0))
2422 .on_press(move |_, _| { *pc.borrow_mut() += 1; })
2423 .on_release(move |_, _| { *rc.borrow_mut() += 1; })
2424 .empty();
2425 ui.eval();
2426 }
2427
2428 ply.context.set_pointer_state(Vector2::new(50.0, 50.0), true);
2430 assert_eq!(*press_count.borrow(), 1, "on_press should fire once");
2431 assert_eq!(*release_count.borrow(), 0, "on_release should not fire yet");
2432
2433 ply.context.set_pointer_state(Vector2::new(50.0, 50.0), false);
2435 assert_eq!(*release_count.borrow(), 1, "on_release should fire once");
2436 }
2437
2438 #[test]
2439 fn test_pressed_query() {
2440 let mut ply = Ply::<()>::new_headless(Dimensions::new(800.0, 600.0));
2441
2442 {
2444 let mut ui = ply.begin();
2445 ui.element()
2446 .id("btn")
2447 .width(fixed!(100.0))
2448 .height(fixed!(100.0))
2449 .empty();
2450 ui.eval();
2451 }
2452
2453 ply.context.set_pointer_state(Vector2::new(50.0, 50.0), true);
2455
2456 {
2458 let mut ui = ply.begin();
2459 ui.element()
2460 .id("btn")
2461 .width(fixed!(100.0))
2462 .height(fixed!(100.0))
2463 .children(|ui| {
2464 assert!(ui.pressed(), "element should report as pressed");
2465 });
2466 ui.eval();
2467 }
2468 }
2469
2470 #[test]
2471 fn test_just_pressed_query_one_frame() {
2472 let mut ply = Ply::<()>::new_headless(Dimensions::new(800.0, 600.0));
2473
2474 {
2476 let mut ui = ply.begin();
2477 ui.element()
2478 .id("btn")
2479 .width(fixed!(100.0))
2480 .height(fixed!(100.0))
2481 .empty();
2482 ui.eval();
2483 }
2484
2485 ply.context.set_pointer_state(Vector2::new(50.0, 50.0), true);
2487
2488 {
2490 let mut ui = ply.begin();
2491 ui.element()
2492 .id("btn")
2493 .width(fixed!(100.0))
2494 .height(fixed!(100.0))
2495 .children(|ui| {
2496 assert!(ui.just_pressed(), "element should report as just pressed");
2497 assert!(
2498 ui.is_just_pressed("btn"),
2499 "ID query should report just pressed"
2500 );
2501 assert!(ui.pressed(), "element should still be pressed");
2502 });
2503 ui.eval();
2504 }
2505
2506 ply.context.set_pointer_state(Vector2::new(50.0, 50.0), true);
2508
2509 {
2511 let mut ui = ply.begin();
2512 ui.element()
2513 .id("btn")
2514 .width(fixed!(100.0))
2515 .height(fixed!(100.0))
2516 .children(|ui| {
2517 assert!(!ui.just_pressed(), "just pressed should clear next frame");
2518 assert!(
2519 !ui.is_just_pressed("btn"),
2520 "ID just pressed should clear next frame"
2521 );
2522 assert!(ui.pressed(), "element should remain pressed while held");
2523 });
2524 ui.eval();
2525 }
2526 }
2527
2528 #[test]
2529 fn test_keyboard_activation_marks_just_pressed() {
2530 let mut ply = Ply::<()>::new_headless(Dimensions::new(800.0, 600.0));
2531
2532 {
2534 let mut ui = ply.begin();
2535 ui.element()
2536 .id("btn")
2537 .width(fixed!(100.0))
2538 .height(fixed!(100.0))
2539 .empty();
2540 ui.eval();
2541 }
2542
2543 ply.set_focus("btn");
2545 ply.context.handle_keyboard_activation(true, false);
2546 assert!(ply.is_pressed("btn"), "keyboard press should set pressed state");
2547
2548 {
2550 let mut ui = ply.begin();
2551 ui.element()
2552 .id("btn")
2553 .width(fixed!(100.0))
2554 .height(fixed!(100.0))
2555 .children(|ui| {
2556 assert!(
2557 ui.just_pressed(),
2558 "keyboard activation should mark just_pressed"
2559 );
2560 assert!(
2561 ui.is_just_pressed("btn"),
2562 "ID query should include keyboard press"
2563 );
2564 assert!(ui.pressed(), "element should be pressed while key is held");
2565 });
2566 ui.eval();
2567 }
2568
2569 {
2571 let mut ui = ply.begin();
2572 ui.element()
2573 .id("btn")
2574 .width(fixed!(100.0))
2575 .height(fixed!(100.0))
2576 .children(|ui| {
2577 assert!(!ui.just_pressed());
2578 assert!(!ui.is_just_pressed("btn"));
2579 assert!(ui.pressed());
2580 });
2581 ui.eval();
2582 }
2583
2584 ply.context.handle_keyboard_activation(false, true);
2586 assert!(!ply.is_pressed("btn"), "keyboard release should clear pressed state");
2587 }
2588
2589 #[test]
2590 fn test_just_released_query_one_frame() {
2591 let mut ply = Ply::<()>::new_headless(Dimensions::new(800.0, 600.0));
2592
2593 {
2595 let mut ui = ply.begin();
2596 ui.element()
2597 .id("btn")
2598 .width(fixed!(100.0))
2599 .height(fixed!(100.0))
2600 .empty();
2601 ui.eval();
2602 }
2603
2604 ply.context.set_pointer_state(Vector2::new(50.0, 50.0), true);
2606 ply.context.set_pointer_state(Vector2::new(50.0, 50.0), false);
2607
2608 {
2610 let mut ui = ply.begin();
2611 ui.element()
2612 .id("btn")
2613 .width(fixed!(100.0))
2614 .height(fixed!(100.0))
2615 .children(|ui| {
2616 assert!(ui.just_pressed(), "element should report as just pressed");
2617 assert!(
2618 ui.is_just_pressed("btn"),
2619 "ID query should report just pressed"
2620 );
2621 assert!(ui.just_released(), "element should report as just released");
2622 assert!(
2623 ui.is_just_released("btn"),
2624 "ID query should report just released"
2625 );
2626 assert!(!ui.pressed(), "element should no longer be pressed");
2627 });
2628 ui.eval();
2629 }
2630
2631 {
2633 let mut ui = ply.begin();
2634 ui.element()
2635 .id("btn")
2636 .width(fixed!(100.0))
2637 .height(fixed!(100.0))
2638 .children(|ui| {
2639 assert!(!ui.just_released(), "just released should clear next frame");
2640 assert!(
2641 !ui.is_just_released("btn"),
2642 "ID query should clear next frame"
2643 );
2644 });
2645 ui.eval();
2646 }
2647 }
2648
2649 #[test]
2650 fn test_keyboard_activation_marks_just_released() {
2651 let mut ply = Ply::<()>::new_headless(Dimensions::new(800.0, 600.0));
2652
2653 {
2655 let mut ui = ply.begin();
2656 ui.element()
2657 .id("btn")
2658 .width(fixed!(100.0))
2659 .height(fixed!(100.0))
2660 .empty();
2661 ui.eval();
2662 }
2663
2664 ply.set_focus("btn");
2666 ply.context.handle_keyboard_activation(true, false);
2667 assert!(ply.is_pressed("btn"), "keyboard press should set pressed state");
2668
2669 ply.context.handle_keyboard_activation(false, true);
2670 assert!(
2671 !ply.is_pressed("btn"),
2672 "keyboard release should clear pressed state"
2673 );
2674
2675 {
2677 let mut ui = ply.begin();
2678 ui.element()
2679 .id("btn")
2680 .width(fixed!(100.0))
2681 .height(fixed!(100.0))
2682 .children(|ui| {
2683 assert!(
2684 ui.just_pressed(),
2685 "keyboard press should mark just_pressed even if released before frame"
2686 );
2687 assert!(
2688 ui.is_just_pressed("btn"),
2689 "ID query should include keyboard just_pressed"
2690 );
2691 assert!(
2692 ui.just_released(),
2693 "keyboard release should mark just_released"
2694 );
2695 assert!(
2696 ui.is_just_released("btn"),
2697 "ID query should include keyboard release"
2698 );
2699 });
2700 ui.eval();
2701 }
2702
2703 {
2705 let mut ui = ply.begin();
2706 ui.element()
2707 .id("btn")
2708 .width(fixed!(100.0))
2709 .height(fixed!(100.0))
2710 .children(|ui| {
2711 assert!(!ui.just_released());
2712 assert!(!ui.is_just_released("btn"));
2713 });
2714 ui.eval();
2715 }
2716 }
2717
2718 #[test]
2719 fn test_tab_navigation_cycles_focus() {
2720 let mut ply = Ply::<()>::new_headless(Dimensions::new(800.0, 600.0));
2721
2722 {
2724 let mut ui = ply.begin();
2725 ui.element()
2726 .id("a")
2727 .width(fixed!(100.0))
2728 .height(fixed!(50.0))
2729 .accessibility(|a| a.button("A"))
2730 .empty();
2731 ui.element()
2732 .id("b")
2733 .width(fixed!(100.0))
2734 .height(fixed!(50.0))
2735 .accessibility(|a| a.button("B"))
2736 .empty();
2737 ui.element()
2738 .id("c")
2739 .width(fixed!(100.0))
2740 .height(fixed!(50.0))
2741 .accessibility(|a| a.button("C"))
2742 .empty();
2743 ui.eval();
2744 }
2745
2746 let id_a = Id::from("a").id;
2747 let id_b = Id::from("b").id;
2748 let id_c = Id::from("c").id;
2749
2750 assert_eq!(ply.focused_element(), None);
2752
2753 ply.context.cycle_focus(false);
2755 assert_eq!(ply.context.focused_element_id, id_a);
2756
2757 ply.context.cycle_focus(false);
2759 assert_eq!(ply.context.focused_element_id, id_b);
2760
2761 ply.context.cycle_focus(false);
2763 assert_eq!(ply.context.focused_element_id, id_c);
2764
2765 ply.context.cycle_focus(false);
2767 assert_eq!(ply.context.focused_element_id, id_a);
2768
2769 ply.context.cycle_focus(true);
2771 assert_eq!(ply.context.focused_element_id, id_c);
2772 }
2773
2774 #[test]
2775 fn test_tab_index_ordering() {
2776 let mut ply = Ply::<()>::new_headless(Dimensions::new(800.0, 600.0));
2777
2778 {
2780 let mut ui = ply.begin();
2781 ui.element()
2782 .id("third")
2783 .width(fixed!(100.0))
2784 .height(fixed!(50.0))
2785 .accessibility(|a| a.button("Third").tab_index(3))
2786 .empty();
2787 ui.element()
2788 .id("first")
2789 .width(fixed!(100.0))
2790 .height(fixed!(50.0))
2791 .accessibility(|a| a.button("First").tab_index(1))
2792 .empty();
2793 ui.element()
2794 .id("second")
2795 .width(fixed!(100.0))
2796 .height(fixed!(50.0))
2797 .accessibility(|a| a.button("Second").tab_index(2))
2798 .empty();
2799 ui.eval();
2800 }
2801
2802 let id_first = Id::from("first").id;
2803 let id_second = Id::from("second").id;
2804 let id_third = Id::from("third").id;
2805
2806 ply.context.cycle_focus(false);
2808 assert_eq!(ply.context.focused_element_id, id_first);
2809 ply.context.cycle_focus(false);
2810 assert_eq!(ply.context.focused_element_id, id_second);
2811 ply.context.cycle_focus(false);
2812 assert_eq!(ply.context.focused_element_id, id_third);
2813 }
2814
2815 #[test]
2816 fn test_arrow_key_navigation() {
2817 let mut ply = Ply::<()>::new_headless(Dimensions::new(800.0, 600.0));
2818 use engine::ArrowDirection;
2819
2820 let id_a = Id::from("a").id;
2821 let id_b = Id::from("b").id;
2822
2823 {
2825 let mut ui = ply.begin();
2826 ui.element()
2827 .id("a")
2828 .width(fixed!(100.0))
2829 .height(fixed!(50.0))
2830 .accessibility(|a| a.button("A").focus_right("b"))
2831 .empty();
2832 ui.element()
2833 .id("b")
2834 .width(fixed!(100.0))
2835 .height(fixed!(50.0))
2836 .accessibility(|a| a.button("B").focus_left("a"))
2837 .empty();
2838 ui.eval();
2839 }
2840
2841 ply.context.set_focus(id_a);
2843 assert_eq!(ply.context.focused_element_id, id_a);
2844
2845 ply.context.arrow_focus(ArrowDirection::Right);
2847 assert_eq!(ply.context.focused_element_id, id_b);
2848
2849 ply.context.arrow_focus(ArrowDirection::Left);
2851 assert_eq!(ply.context.focused_element_id, id_a);
2852
2853 ply.context.arrow_focus(ArrowDirection::Up);
2855 assert_eq!(ply.context.focused_element_id, id_a);
2856 }
2857
2858 #[test]
2859 fn test_focused_query() {
2860 let mut ply = Ply::<()>::new_headless(Dimensions::new(800.0, 600.0));
2861
2862 let id_a = Id::from("a").id;
2863
2864 {
2866 let mut ui = ply.begin();
2867 ui.element()
2868 .id("a")
2869 .width(fixed!(100.0))
2870 .height(fixed!(50.0))
2871 .accessibility(|a| a.button("A"))
2872 .empty();
2873 ui.eval();
2874 }
2875
2876 ply.context.set_focus(id_a);
2877
2878 {
2880 let mut ui = ply.begin();
2881 ui.element()
2882 .id("a")
2883 .width(fixed!(100.0))
2884 .height(fixed!(50.0))
2885 .accessibility(|a| a.button("A"))
2886 .children(|ui| {
2887 assert!(ui.focused(), "element should report as focused");
2888 });
2889 ui.eval();
2890 }
2891 }
2892
2893 #[test]
2894 fn test_on_focus_callback_fires_on_tab() {
2895 use std::cell::RefCell;
2896 use std::rc::Rc;
2897
2898 let mut ply = Ply::<()>::new_headless(Dimensions::new(800.0, 600.0));
2899 let focus_a = Rc::new(RefCell::new(0u32));
2900 let unfocus_a = Rc::new(RefCell::new(0u32));
2901 let focus_b = Rc::new(RefCell::new(0u32));
2902
2903 {
2905 let fa = focus_a.clone();
2906 let ua = unfocus_a.clone();
2907 let fb = focus_b.clone();
2908 let mut ui = ply.begin();
2909 ui.element()
2910 .id("a")
2911 .width(fixed!(100.0))
2912 .height(fixed!(50.0))
2913 .accessibility(|a| a.button("A"))
2914 .on_focus(move |_| { *fa.borrow_mut() += 1; })
2915 .on_unfocus(move |_| { *ua.borrow_mut() += 1; })
2916 .empty();
2917 ui.element()
2918 .id("b")
2919 .width(fixed!(100.0))
2920 .height(fixed!(50.0))
2921 .accessibility(|a| a.button("B"))
2922 .on_focus(move |_| { *fb.borrow_mut() += 1; })
2923 .empty();
2924 ui.eval();
2925 }
2926
2927 ply.context.cycle_focus(false);
2929 assert_eq!(*focus_a.borrow(), 1, "on_focus should fire for A");
2930 assert_eq!(*unfocus_a.borrow(), 0, "on_unfocus should not fire yet");
2931
2932 ply.context.cycle_focus(false);
2934 assert_eq!(*unfocus_a.borrow(), 1, "on_unfocus should fire for A");
2935 assert_eq!(*focus_b.borrow(), 1, "on_focus should fire for B");
2936 }
2937
2938 #[test]
2939 fn test_on_focus_callback_fires_on_set_focus() {
2940 use std::cell::RefCell;
2941 use std::rc::Rc;
2942
2943 let mut ply = Ply::<()>::new_headless(Dimensions::new(800.0, 600.0));
2944 let focus_count = Rc::new(RefCell::new(0u32));
2945 let unfocus_count = Rc::new(RefCell::new(0u32));
2946
2947 let id_a = Id::from("a").id;
2948
2949 {
2951 let fc = focus_count.clone();
2952 let uc = unfocus_count.clone();
2953 let mut ui = ply.begin();
2954 ui.element()
2955 .id("a")
2956 .width(fixed!(100.0))
2957 .height(fixed!(50.0))
2958 .accessibility(|a| a.button("A"))
2959 .on_focus(move |_| { *fc.borrow_mut() += 1; })
2960 .on_unfocus(move |_| { *uc.borrow_mut() += 1; })
2961 .empty();
2962 ui.eval();
2963 }
2964
2965 ply.context.set_focus(id_a);
2967 assert_eq!(*focus_count.borrow(), 1, "on_focus should fire on set_focus");
2968
2969 ply.context.clear_focus();
2971 assert_eq!(*unfocus_count.borrow(), 1, "on_unfocus should fire on clear_focus");
2972 }
2973
2974 #[test]
2975 fn test_focus_ring_render_command() {
2976 use render_commands::RenderCommandConfig;
2977
2978 let mut ply = Ply::<()>::new_headless(Dimensions::new(800.0, 600.0));
2979 let id_a = Id::from("a").id;
2980
2981 {
2983 let mut ui = ply.begin();
2984 ui.element()
2985 .id("a")
2986 .width(fixed!(100.0))
2987 .height(fixed!(50.0))
2988 .corner_radius(8.0)
2989 .accessibility(|a| a.button("A"))
2990 .empty();
2991 ui.eval();
2992 }
2993
2994 ply.context.focus_from_keyboard = true;
2996 ply.context.set_focus(id_a);
2997
2998 {
3000 let mut ui = ply.begin();
3001 ui.element()
3002 .id("a")
3003 .width(fixed!(100.0))
3004 .height(fixed!(50.0))
3005 .corner_radius(8.0)
3006 .accessibility(|a| a.button("A"))
3007 .empty();
3008 let items = ui.eval();
3009
3010 let focus_ring = items.iter().find(|cmd| {
3012 cmd.z_index == 32764 && matches!(cmd.config, RenderCommandConfig::Border(_))
3013 });
3014 assert!(focus_ring.is_some(), "Focus ring border should be in render commands");
3015
3016 let ring = focus_ring.unwrap();
3017 assert!(ring.bounding_box.width > 100.0, "Focus ring should be wider than element");
3019 assert!(ring.bounding_box.height > 50.0, "Focus ring should be taller than element");
3020 }
3021 }
3022
3023 #[test]
3024 fn test_overflow_scrollbar_renders_and_moves_with_set_scroll_position() {
3025 let mut ply = Ply::<()>::new_headless(Dimensions::new(400.0, 300.0));
3026
3027 {
3028 let mut ui = ply.begin();
3029 ui.element()
3030 .id("scroll")
3031 .width(fixed!(100.0))
3032 .height(fixed!(100.0))
3033 .overflow(|o| o.scroll().scrollbar(|s| s))
3034 .children(|ui| {
3035 ui.element()
3036 .width(fixed!(300.0))
3037 .height(fixed!(250.0))
3038 .empty();
3039 });
3040 let items = ui.eval();
3041
3042 let rects: Vec<_> = items
3043 .iter()
3044 .filter(|cmd| matches!(cmd.config, render_commands::RenderCommandConfig::Rectangle(_)))
3045 .collect();
3046 assert!(rects.len() >= 2, "Expected scrollbar thumb rectangles");
3047
3048 let v_thumb = rects
3049 .iter()
3050 .find(|cmd| (cmd.bounding_box.width - 6.0).abs() < 0.1 && cmd.bounding_box.height > cmd.bounding_box.width)
3051 .expect("Expected vertical scrollbar thumb");
3052 let h_thumb = rects
3053 .iter()
3054 .find(|cmd| (cmd.bounding_box.height - 6.0).abs() < 0.1 && cmd.bounding_box.width > cmd.bounding_box.height)
3055 .expect("Expected horizontal scrollbar thumb");
3056
3057 assert!((v_thumb.bounding_box.x - 94.0).abs() < 0.1);
3058 assert!((h_thumb.bounding_box.y - 94.0).abs() < 0.1);
3059 }
3060
3061 ply.set_scroll_position("scroll", (80.0, 90.0));
3062
3063 {
3064 let mut ui = ply.begin();
3065 ui.element()
3066 .id("scroll")
3067 .width(fixed!(100.0))
3068 .height(fixed!(100.0))
3069 .overflow(|o| o.scroll().scrollbar(|s| s))
3070 .children(|ui| {
3071 ui.element()
3072 .width(fixed!(300.0))
3073 .height(fixed!(250.0))
3074 .empty();
3075 });
3076 let items = ui.eval();
3077
3078 let rects: Vec<_> = items
3079 .iter()
3080 .filter(|cmd| matches!(cmd.config, render_commands::RenderCommandConfig::Rectangle(_)))
3081 .collect();
3082
3083 let v_thumb = rects
3084 .iter()
3085 .find(|cmd| (cmd.bounding_box.width - 6.0).abs() < 0.1 && cmd.bounding_box.height > cmd.bounding_box.width)
3086 .expect("Expected vertical scrollbar thumb");
3087 let h_thumb = rects
3088 .iter()
3089 .find(|cmd| (cmd.bounding_box.height - 6.0).abs() < 0.1 && cmd.bounding_box.width > cmd.bounding_box.height)
3090 .expect("Expected horizontal scrollbar thumb");
3091
3092 assert!(v_thumb.bounding_box.y > 0.0);
3093 assert!(h_thumb.bounding_box.x > 0.0);
3094 }
3095 }
3096}