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 is_down = macroquad::prelude::is_mouse_button_down(
577 macroquad::prelude::MouseButton::Left,
578 );
579
580 self.context.set_pointer_state(Vector2::new(mx, my), is_down);
583
584 {
585 use macroquad::prelude::{is_key_down, KeyCode};
586 let shift = is_key_down(KeyCode::LeftShift) || is_key_down(KeyCode::RightShift);
587 if shift {
588 if let Some(ref mut pending) = self.context.pending_text_click {
590 pending.3 = true;
591 }
592 }
593 }
594
595 let (scroll_x, scroll_y) = macroquad::prelude::mouse_wheel();
596 #[cfg(target_arch = "wasm32")]
597 const SCROLL_SPEED: f32 = 1.0;
598 #[cfg(not(target_arch = "wasm32"))]
599 const SCROLL_SPEED: f32 = 20.0;
600 let scroll_shift = {
602 use macroquad::prelude::{is_key_down, KeyCode};
603 is_key_down(KeyCode::LeftShift) || is_key_down(KeyCode::RightShift)
604 };
605 let scroll_delta = if scroll_shift {
606 Vector2::new(
608 (scroll_x + scroll_y) * SCROLL_SPEED,
609 0.0,
610 )
611 } else {
612 Vector2::new(scroll_x * SCROLL_SPEED, scroll_y * SCROLL_SPEED)
613 };
614 let touch_input_active = !macroquad::prelude::touches().is_empty();
615
616 let text_consumed_scroll = self.context.update_text_input_pointer_scroll(
618 scroll_delta,
619 touch_input_active,
620 );
621 self.context.clamp_text_input_scroll();
622
623 let container_scroll = if text_consumed_scroll {
625 Vector2::new(0.0, 0.0)
626 } else {
627 scroll_delta
628 };
629 self.context.update_scroll_containers(
630 true,
631 container_scroll,
632 macroquad::prelude::get_frame_time(),
633 touch_input_active,
634 );
635
636 use macroquad::prelude::{is_key_pressed, is_key_down, is_key_released, KeyCode};
638
639 let text_input_focused = self.context.is_text_input_focused();
640 let current_focused_id = self.context.focused_element_id;
641
642 if current_focused_id != self.text_input_repeat_focus_id {
645 self.text_input_repeat_key = 0;
646 self.text_input_repeat_focus_id = current_focused_id;
647 }
648
649 if is_key_pressed(KeyCode::Tab) {
651 let shift = is_key_down(KeyCode::LeftShift) || is_key_down(KeyCode::RightShift);
652 self.context.cycle_focus(shift);
653 } else if text_input_focused {
654 let shift = is_key_down(KeyCode::LeftShift) || is_key_down(KeyCode::RightShift);
656 let ctrl = is_key_down(KeyCode::LeftControl) || is_key_down(KeyCode::RightControl);
657 let time = self.context.current_time;
658
659 const INITIAL_DELAY: f64 = 0.5;
661 const REPEAT_INTERVAL: f64 = 0.033;
662
663 macro_rules! key_fires {
665 ($key:expr, $id:expr) => {{
666 if is_key_pressed($key) {
667 self.text_input_repeat_key = $id;
668 self.text_input_repeat_first = time;
669 self.text_input_repeat_last = time;
670 true
671 } else if is_key_down($key) && self.text_input_repeat_key == $id {
672 let since_first = time - self.text_input_repeat_first;
673 let since_last = time - self.text_input_repeat_last;
674 if since_first > INITIAL_DELAY && since_last > REPEAT_INTERVAL {
675 self.text_input_repeat_last = time;
676 true
677 } else {
678 false
679 }
680 } else {
681 false
682 }
683 }};
684 }
685
686 let mut cursor_moved = false;
688 if key_fires!(KeyCode::Left, 1) {
689 if ctrl {
690 self.context.process_text_input_action(engine::TextInputAction::MoveWordLeft { shift });
691 } else {
692 self.context.process_text_input_action(engine::TextInputAction::MoveLeft { shift });
693 }
694 cursor_moved = true;
695 }
696 if key_fires!(KeyCode::Right, 2) {
697 if ctrl {
698 self.context.process_text_input_action(engine::TextInputAction::MoveWordRight { shift });
699 } else {
700 self.context.process_text_input_action(engine::TextInputAction::MoveRight { shift });
701 }
702 cursor_moved = true;
703 }
704 if key_fires!(KeyCode::Backspace, 3) {
705 if ctrl {
706 self.context.process_text_input_action(engine::TextInputAction::BackspaceWord);
707 } else {
708 self.context.process_text_input_action(engine::TextInputAction::Backspace);
709 }
710 cursor_moved = true;
711 }
712 if key_fires!(KeyCode::Delete, 4) {
713 if ctrl {
714 self.context.process_text_input_action(engine::TextInputAction::DeleteWord);
715 } else {
716 self.context.process_text_input_action(engine::TextInputAction::Delete);
717 }
718 cursor_moved = true;
719 }
720 if key_fires!(KeyCode::Home, 5) {
721 self.context.process_text_input_action(engine::TextInputAction::MoveHome { shift });
722 cursor_moved = true;
723 }
724 if key_fires!(KeyCode::End, 6) {
725 self.context.process_text_input_action(engine::TextInputAction::MoveEnd { shift });
726 cursor_moved = true;
727 }
728
729 if self.context.is_focused_text_input_multiline() {
731 if key_fires!(KeyCode::Up, 7) {
732 self.context.process_text_input_action(engine::TextInputAction::MoveUp { shift });
733 cursor_moved = true;
734 }
735 if key_fires!(KeyCode::Down, 8) {
736 self.context.process_text_input_action(engine::TextInputAction::MoveDown { shift });
737 cursor_moved = true;
738 }
739 }
740
741 if is_key_pressed(KeyCode::Enter) {
743 self.context.process_text_input_action(engine::TextInputAction::Submit);
744 cursor_moved = true;
745 }
746 if ctrl && is_key_pressed(KeyCode::A) {
747 self.context.process_text_input_action(engine::TextInputAction::SelectAll);
748 }
749 if ctrl && is_key_pressed(KeyCode::Z) {
750 if shift {
751 self.context.process_text_input_action(engine::TextInputAction::Redo);
752 } else {
753 self.context.process_text_input_action(engine::TextInputAction::Undo);
754 }
755 cursor_moved = true;
756 }
757 if ctrl && is_key_pressed(KeyCode::Y) {
758 self.context.process_text_input_action(engine::TextInputAction::Redo);
759 cursor_moved = true;
760 }
761 if ctrl && is_key_pressed(KeyCode::C) {
762 let elem_id = self.context.focused_element_id;
764 if let Some(state) = self.context.text_edit_states.get(&elem_id) {
765 #[cfg(feature = "text-styling")]
766 let selected = state.selected_text_styled();
767 #[cfg(not(feature = "text-styling"))]
768 let selected = state.selected_text().to_string();
769 if !selected.is_empty() {
770 macroquad::miniquad::window::clipboard_set(&selected);
771 }
772 }
773 }
774 if ctrl && is_key_pressed(KeyCode::X) {
775 let elem_id = self.context.focused_element_id;
777 if let Some(state) = self.context.text_edit_states.get(&elem_id) {
778 #[cfg(feature = "text-styling")]
779 let selected = state.selected_text_styled();
780 #[cfg(not(feature = "text-styling"))]
781 let selected = state.selected_text().to_string();
782 if !selected.is_empty() {
783 macroquad::miniquad::window::clipboard_set(&selected);
784 }
785 }
786 self.context.process_text_input_action(engine::TextInputAction::Cut);
787 cursor_moved = true;
788 }
789 if ctrl && is_key_pressed(KeyCode::V) {
790 if let Some(text) = macroquad::miniquad::window::clipboard_get() {
792 self.context.process_text_input_action(engine::TextInputAction::Paste { text });
793 cursor_moved = true;
794 }
795 }
796
797 if is_key_pressed(KeyCode::Escape) {
799 self.context.clear_focus();
800 }
801
802 if self.text_input_repeat_key != 0 {
804 let still_down = match self.text_input_repeat_key {
805 1 => is_key_down(KeyCode::Left),
806 2 => is_key_down(KeyCode::Right),
807 3 => is_key_down(KeyCode::Backspace),
808 4 => is_key_down(KeyCode::Delete),
809 5 => is_key_down(KeyCode::Home),
810 6 => is_key_down(KeyCode::End),
811 7 => is_key_down(KeyCode::Up),
812 8 => is_key_down(KeyCode::Down),
813 _ => false,
814 };
815 if !still_down {
816 self.text_input_repeat_key = 0;
817 }
818 }
819
820 while let Some(ch) = macroquad::prelude::get_char_pressed() {
822 if !ch.is_control() && !ctrl {
824 self.context.process_text_input_char(ch);
825 cursor_moved = true;
826 }
827 }
828
829 if cursor_moved {
832 self.context.update_text_input_scroll();
833 }
834 self.context.clamp_text_input_scroll();
835 } else {
836 if is_key_pressed(KeyCode::Right) { self.context.arrow_focus(engine::ArrowDirection::Right); }
838 if is_key_pressed(KeyCode::Left) { self.context.arrow_focus(engine::ArrowDirection::Left); }
839 if is_key_pressed(KeyCode::Up) { self.context.arrow_focus(engine::ArrowDirection::Up); }
840 if is_key_pressed(KeyCode::Down) { self.context.arrow_focus(engine::ArrowDirection::Down); }
841
842 let activate_pressed = is_key_pressed(KeyCode::Enter) || is_key_pressed(KeyCode::Space);
843 let activate_released = is_key_released(KeyCode::Enter) || is_key_released(KeyCode::Space);
844 self.context.handle_keyboard_activation(activate_pressed, activate_released);
845 }
846 }
847
848 {
850 let text_input_focused = self.context.is_text_input_focused();
851 if text_input_focused != self.was_text_input_focused {
852 #[cfg(not(any(target_arch = "wasm32", target_os = "linux")))]
853 {
854 macroquad::miniquad::window::show_keyboard(text_input_focused);
855 }
856 #[cfg(target_arch = "wasm32")]
857 {
858 unsafe { ply_show_virtual_keyboard(text_input_focused); }
859 }
860 self.was_text_input_focused = text_input_focused;
861 }
862 }
863
864 self.context.begin_layout();
865 Ui {
866 ply: self,
867 }
868 }
869
870 pub async fn new(default_font: &'static renderer::FontAsset) -> Self {
872 renderer::FontManager::load_default(default_font).await;
873
874 let dimensions = Dimensions::new(
875 macroquad::prelude::screen_width(),
876 macroquad::prelude::screen_height(),
877 );
878 let mut ply = Self {
879 context: engine::PlyContext::new(dimensions),
880 headless: false,
881 text_input_repeat_key: 0,
882 text_input_repeat_first: 0.0,
883 text_input_repeat_last: 0.0,
884 text_input_repeat_focus_id: 0,
885 was_text_input_focused: false,
886 #[cfg(all(feature = "a11y", target_arch = "wasm32"))]
887 web_a11y_state: accessibility_web::WebAccessibilityState::default(),
888 #[cfg(all(feature = "a11y", not(target_arch = "wasm32")))]
889 native_a11y_state: accessibility_native::NativeAccessibilityState::default(),
890 };
891 ply.context.default_font_key = default_font.key();
892 ply.set_measure_text_function(renderer::create_measure_text_function());
893 ply
894 }
895
896 pub fn new_headless(dimensions: Dimensions) -> Self {
901 Self {
902 context: engine::PlyContext::new(dimensions),
903 headless: true,
904 text_input_repeat_key: 0,
905 text_input_repeat_first: 0.0,
906 text_input_repeat_last: 0.0,
907 text_input_repeat_focus_id: 0,
908 was_text_input_focused: false,
909 #[cfg(all(feature = "a11y", target_arch = "wasm32"))]
910 web_a11y_state: accessibility_web::WebAccessibilityState::default(),
911 #[cfg(all(feature = "a11y", not(target_arch = "wasm32")))]
912 native_a11y_state: accessibility_native::NativeAccessibilityState::default(),
913 }
914 }
915
916 pub fn pointer_over(&self, cfg: impl Into<Id>) -> bool {
918 self.context.pointer_over(cfg.into())
919 }
920
921 pub fn pointer_over_ids(&self) -> Vec<Id> {
923 self.context.get_pointer_over_ids().to_vec()
924 }
925
926 pub fn set_measure_text_function<F>(&mut self, callback: F)
928 where
929 F: Fn(&str, &TextConfig) -> Dimensions + 'static,
930 {
931 self.context.set_measure_text_function(Box::new(
932 move |text: &str, config: &TextConfig| -> Dimensions {
933 callback(text, config)
934 },
935 ));
936 }
937
938 pub fn max_element_count(&mut self, max_element_count: u32) {
941 self.context.set_max_element_count(max_element_count as i32);
942 }
943
944 pub fn max_measure_text_cache_word_count(&mut self, count: u32) {
947 self.context.set_max_measure_text_cache_word_count(count as i32);
948 }
949
950 pub fn set_debug_mode(&mut self, enable: bool) {
952 self.context.set_debug_mode_enabled(enable);
953 }
954
955 pub fn set_debug_view_width(&mut self, width: f32) {
957 self.context.set_debug_view_width(width);
958 }
959
960 pub fn is_debug_mode(&self) -> bool {
962 self.context.is_debug_mode_enabled()
963 }
964
965 pub fn set_culling(&mut self, enable: bool) {
967 self.context.set_culling_enabled(enable);
968 }
969
970 pub fn set_layout_dimensions(&mut self, dimensions: Dimensions) {
973 self.context.set_layout_dimensions(dimensions);
974 }
975
976 pub fn pointer_state(&mut self, position: Vector2, is_down: bool) {
979 self.context.set_pointer_state(position, is_down);
980 }
981
982 pub fn update_scroll_containers(
984 &mut self,
985 drag_scrolling_enabled: bool,
986 scroll_delta: Vector2,
987 delta_time: f32,
988 ) {
989 let touch_input_active = if self.headless {
990 false
991 } else {
992 !macroquad::prelude::touches().is_empty()
993 };
994 self.context
995 .update_scroll_containers(drag_scrolling_enabled, scroll_delta, delta_time, touch_input_active);
996 }
997
998 pub fn focused_element(&self) -> Option<Id> {
1000 self.context.focused_element()
1001 }
1002
1003 pub fn set_focus(&mut self, id: impl Into<Id>) {
1005 self.context.set_focus(id.into().id);
1006 }
1007
1008 pub fn clear_focus(&mut self) {
1010 self.context.clear_focus();
1011 }
1012
1013 pub fn get_text_value(&self, id: impl Into<Id>) -> &str {
1016 self.context.get_text_value(id.into().id)
1017 }
1018
1019 pub fn set_text_value(&mut self, id: impl Into<Id>, value: &str) {
1021 self.context.set_text_value(id.into().id, value);
1022 }
1023
1024 pub fn get_cursor_pos(&self, id: impl Into<Id>) -> usize {
1027 self.context.get_cursor_pos(id.into().id)
1028 }
1029
1030 pub fn set_cursor_pos(&mut self, id: impl Into<Id>, pos: usize) {
1033 self.context.set_cursor_pos(id.into().id, pos);
1034 }
1035
1036 pub fn get_selection_range(&self, id: impl Into<Id>) -> Option<(usize, usize)> {
1038 self.context.get_selection_range(id.into().id)
1039 }
1040
1041 pub fn set_selection(&mut self, id: impl Into<Id>, anchor: usize, cursor: usize) {
1044 self.context.set_selection(id.into().id, anchor, cursor);
1045 }
1046
1047 pub fn is_pressed(&self, id: impl Into<Id>) -> bool {
1049 self.context.is_element_pressed(id.into().id)
1050 }
1051
1052 pub fn is_just_pressed(&self, id: impl Into<Id>) -> bool {
1054 self.context.is_element_just_pressed(id.into().id)
1055 }
1056
1057 pub fn is_just_released(&self, id: impl Into<Id>) -> bool {
1059 self.context.is_element_just_released(id.into().id)
1060 }
1061
1062 pub fn bounding_box(&self, id: impl Into<Id>) -> Option<math::BoundingBox> {
1064 self.context.get_element_data(id.into())
1065 }
1066
1067 pub fn scroll_container_data(&self, id: impl Into<Id>) -> Option<engine::ScrollContainerData> {
1069 let data = self.context.get_scroll_container_data(id.into());
1070 if data.found {
1071 Some(data)
1072 } else {
1073 None
1074 }
1075 }
1076
1077 pub fn set_scroll_position(&mut self, id: impl Into<Id>, position: impl Into<Vector2>) {
1079 self.context.set_scroll_position(id.into(), position.into());
1080 }
1081
1082 pub fn eval(&mut self) -> Vec<RenderCommand<CustomElementData>> {
1084 #[cfg(feature = "net")]
1086 net::NET_MANAGER.lock().unwrap().clean();
1087
1088 let commands = self.context.end_layout();
1089 let mut result = Vec::new();
1090 for cmd in commands {
1091 result.push(RenderCommand::from_engine_render_command(cmd));
1092 }
1093
1094 #[cfg(all(feature = "a11y", target_arch = "wasm32"))]
1096 {
1097 let accessibility_bounds = self.accessibility_bounds();
1098
1099 accessibility_web::sync_accessibility_tree(
1100 &mut self.web_a11y_state,
1101 &self.context.accessibility_configs,
1102 &accessibility_bounds,
1103 &self.context.accessibility_element_order,
1104 self.context.focused_element_id,
1105 self.context.layout_dimensions,
1106 );
1107 }
1108
1109 #[cfg(all(feature = "a11y", not(target_arch = "wasm32")))]
1111 {
1112 let accessibility_bounds = self.accessibility_bounds();
1113
1114 let a11y_actions = accessibility_native::sync_accessibility_tree(
1115 &mut self.native_a11y_state,
1116 &self.context.accessibility_configs,
1117 &accessibility_bounds,
1118 &self.context.accessibility_element_order,
1119 self.context.focused_element_id,
1120 self.context.layout_dimensions,
1121 );
1122 for action in a11y_actions {
1123 match action {
1124 accessibility_native::PendingA11yAction::Focus(target_id) => {
1125 self.context.change_focus(target_id);
1126 }
1127 accessibility_native::PendingA11yAction::Click(target_id) => {
1128 self.context.fire_press(target_id);
1129 }
1130 }
1131 }
1132 }
1133
1134 result
1135 }
1136
1137 pub async fn show(
1139 &mut self,
1140 handle_custom_command: impl Fn(&RenderCommand<CustomElementData>),
1141 ) {
1142 let commands = self.eval();
1143 renderer::render(commands, handle_custom_command).await;
1144 }
1145}
1146
1147#[cfg(target_arch = "wasm32")]
1148extern "C" {
1149 fn ply_show_virtual_keyboard(show: bool);
1150}
1151
1152#[cfg(test)]
1153mod tests {
1154 use super::*;
1155 use color::Color;
1156 use layout::{Padding, Sizing};
1157
1158 #[rustfmt::skip]
1159 #[test]
1160 fn test_begin() {
1161 let mut ply = Ply::<()>::new_headless(Dimensions::new(800.0, 600.0));
1162
1163 ply.set_measure_text_function(|_, _| {
1164 Dimensions::new(100.0, 24.0)
1165 });
1166
1167 let mut ui = ply.begin();
1168
1169 ui.element().width(fixed!(100.0)).height(fixed!(100.0))
1170 .background_color(0xFFFFFF)
1171 .children(|ui| {
1172 ui.element().width(fixed!(100.0)).height(fixed!(100.0))
1173 .background_color(0xFFFFFF)
1174 .children(|ui| {
1175 ui.element().width(fixed!(100.0)).height(fixed!(100.0))
1176 .background_color(0xFFFFFF)
1177 .children(|ui| {
1178 ui.text("test", |t| t
1179 .color(0xFFFFFF)
1180 .font_size(24)
1181 );
1182 });
1183 });
1184 });
1185
1186 ui.element()
1187 .border(|b| b
1188 .color(0xFFFF00)
1189 .all(2)
1190 )
1191 .corner_radius(10.0)
1192 .children(|ui| {
1193 ui.element().width(fixed!(50.0)).height(fixed!(50.0))
1194 .background_color(0x00FFFF)
1195 .empty();
1196 });
1197
1198 let items = ui.eval();
1199
1200 for item in &items {
1201 println!(
1202 "id: {}\nbbox: {:?}\nconfig: {:?}",
1203 item.id, item.bounding_box, item.config,
1204 );
1205 }
1206
1207 assert_eq!(items.len(), 6);
1208
1209 assert_eq!(items[0].bounding_box.x, 0.0);
1210 assert_eq!(items[0].bounding_box.y, 0.0);
1211 assert_eq!(items[0].bounding_box.width, 100.0);
1212 assert_eq!(items[0].bounding_box.height, 100.0);
1213 match &items[0].config {
1214 render_commands::RenderCommandConfig::Rectangle(rect) => {
1215 assert_eq!(rect.color.r, 255.0);
1216 assert_eq!(rect.color.g, 255.0);
1217 assert_eq!(rect.color.b, 255.0);
1218 assert_eq!(rect.color.a, 255.0);
1219 }
1220 _ => panic!("Expected Rectangle config for item 0"),
1221 }
1222
1223 assert_eq!(items[1].bounding_box.x, 0.0);
1224 assert_eq!(items[1].bounding_box.y, 0.0);
1225 assert_eq!(items[1].bounding_box.width, 100.0);
1226 assert_eq!(items[1].bounding_box.height, 100.0);
1227 match &items[1].config {
1228 render_commands::RenderCommandConfig::Rectangle(rect) => {
1229 assert_eq!(rect.color.r, 255.0);
1230 assert_eq!(rect.color.g, 255.0);
1231 assert_eq!(rect.color.b, 255.0);
1232 assert_eq!(rect.color.a, 255.0);
1233 }
1234 _ => panic!("Expected Rectangle config for item 1"),
1235 }
1236
1237 assert_eq!(items[2].bounding_box.x, 0.0);
1238 assert_eq!(items[2].bounding_box.y, 0.0);
1239 assert_eq!(items[2].bounding_box.width, 100.0);
1240 assert_eq!(items[2].bounding_box.height, 100.0);
1241 match &items[2].config {
1242 render_commands::RenderCommandConfig::Rectangle(rect) => {
1243 assert_eq!(rect.color.r, 255.0);
1244 assert_eq!(rect.color.g, 255.0);
1245 assert_eq!(rect.color.b, 255.0);
1246 assert_eq!(rect.color.a, 255.0);
1247 }
1248 _ => panic!("Expected Rectangle config for item 2"),
1249 }
1250
1251 assert_eq!(items[3].bounding_box.x, 0.0);
1252 assert_eq!(items[3].bounding_box.y, 0.0);
1253 assert_eq!(items[3].bounding_box.width, 100.0);
1254 assert_eq!(items[3].bounding_box.height, 24.0);
1255 match &items[3].config {
1256 render_commands::RenderCommandConfig::Text(text) => {
1257 assert_eq!(text.text, "test");
1258 assert_eq!(text.color.r, 255.0);
1259 assert_eq!(text.color.g, 255.0);
1260 assert_eq!(text.color.b, 255.0);
1261 assert_eq!(text.color.a, 255.0);
1262 assert_eq!(text.font_size, 24);
1263 }
1264 _ => panic!("Expected Text config for item 3"),
1265 }
1266
1267 assert_eq!(items[4].bounding_box.x, 100.0);
1268 assert_eq!(items[4].bounding_box.y, 0.0);
1269 assert_eq!(items[4].bounding_box.width, 50.0);
1270 assert_eq!(items[4].bounding_box.height, 50.0);
1271 match &items[4].config {
1272 render_commands::RenderCommandConfig::Rectangle(rect) => {
1273 assert_eq!(rect.color.r, 0.0);
1274 assert_eq!(rect.color.g, 255.0);
1275 assert_eq!(rect.color.b, 255.0);
1276 assert_eq!(rect.color.a, 255.0);
1277 }
1278 _ => panic!("Expected Rectangle config for item 4"),
1279 }
1280
1281 assert_eq!(items[5].bounding_box.x, 100.0);
1282 assert_eq!(items[5].bounding_box.y, 0.0);
1283 assert_eq!(items[5].bounding_box.width, 50.0);
1284 assert_eq!(items[5].bounding_box.height, 50.0);
1285 match &items[5].config {
1286 render_commands::RenderCommandConfig::Border(border) => {
1287 assert_eq!(border.color.r, 255.0);
1288 assert_eq!(border.color.g, 255.0);
1289 assert_eq!(border.color.b, 0.0);
1290 assert_eq!(border.color.a, 255.0);
1291 assert_eq!(border.corner_radii.top_left, 10.0);
1292 assert_eq!(border.corner_radii.top_right, 10.0);
1293 assert_eq!(border.corner_radii.bottom_left, 10.0);
1294 assert_eq!(border.corner_radii.bottom_right, 10.0);
1295 assert_eq!(border.width.left, 2);
1296 assert_eq!(border.width.right, 2);
1297 assert_eq!(border.width.top, 2);
1298 assert_eq!(border.width.bottom, 2);
1299 }
1300 _ => panic!("Expected Border config for item 5"),
1301 }
1302 }
1303
1304 #[rustfmt::skip]
1305 #[test]
1306 fn test_example() {
1307 let mut ply = Ply::<()>::new_headless(Dimensions::new(1000.0, 1000.0));
1308
1309 let mut ui = ply.begin();
1310
1311 ui.set_measure_text_function(|_, _| {
1312 Dimensions::new(100.0, 24.0)
1313 });
1314
1315 for &(label, level) in &[("Road", 1), ("Wall", 2), ("Tower", 3)] {
1316 ui.element().width(grow!()).height(fixed!(36.0))
1317 .layout(|l| l
1318 .direction(crate::layout::LayoutDirection::LeftToRight)
1319 .gap(12)
1320 .align(crate::align::AlignX::Left, crate::align::AlignY::CenterY)
1321 )
1322 .children(|ui| {
1323 ui.text(label, |t| t
1324 .font_size(18)
1325 .color(0xFFFFFF)
1326 );
1327 ui.element().width(grow!()).height(fixed!(18.0))
1328 .corner_radius(9.0)
1329 .background_color(0x555555)
1330 .children(|ui| {
1331 ui.element()
1332 .width(fixed!(300.0 * level as f32 / 3.0))
1333 .height(grow!())
1334 .corner_radius(9.0)
1335 .background_color(0x45A85A)
1336 .empty();
1337 });
1338 });
1339 }
1340
1341 let items = ui.eval();
1342
1343 for item in &items {
1344 println!(
1345 "id: {}\nbbox: {:?}\nconfig: {:?}",
1346 item.id, item.bounding_box, item.config,
1347 );
1348 }
1349
1350 assert_eq!(items.len(), 9);
1351
1352 assert_eq!(items[0].bounding_box.x, 0.0);
1354 assert_eq!(items[0].bounding_box.y, 6.0);
1355 assert_eq!(items[0].bounding_box.width, 100.0);
1356 assert_eq!(items[0].bounding_box.height, 24.0);
1357 match &items[0].config {
1358 render_commands::RenderCommandConfig::Text(text) => {
1359 assert_eq!(text.text, "Road");
1360 assert_eq!(text.color.r, 255.0);
1361 assert_eq!(text.color.g, 255.0);
1362 assert_eq!(text.color.b, 255.0);
1363 assert_eq!(text.color.a, 255.0);
1364 assert_eq!(text.font_size, 18);
1365 }
1366 _ => panic!("Expected Text config for item 0"),
1367 }
1368
1369 assert_eq!(items[1].bounding_box.x, 112.0);
1371 assert_eq!(items[1].bounding_box.y, 9.0);
1372 assert_eq!(items[1].bounding_box.width, 163.99142);
1373 assert_eq!(items[1].bounding_box.height, 18.0);
1374 match &items[1].config {
1375 render_commands::RenderCommandConfig::Rectangle(rect) => {
1376 assert_eq!(rect.color.r, 85.0);
1377 assert_eq!(rect.color.g, 85.0);
1378 assert_eq!(rect.color.b, 85.0);
1379 assert_eq!(rect.color.a, 255.0);
1380 assert_eq!(rect.corner_radii.top_left, 9.0);
1381 assert_eq!(rect.corner_radii.top_right, 9.0);
1382 assert_eq!(rect.corner_radii.bottom_left, 9.0);
1383 assert_eq!(rect.corner_radii.bottom_right, 9.0);
1384 }
1385 _ => panic!("Expected Rectangle config for item 1"),
1386 }
1387
1388 assert_eq!(items[2].bounding_box.x, 112.0);
1390 assert_eq!(items[2].bounding_box.y, 9.0);
1391 assert_eq!(items[2].bounding_box.width, 100.0);
1392 assert_eq!(items[2].bounding_box.height, 18.0);
1393 match &items[2].config {
1394 render_commands::RenderCommandConfig::Rectangle(rect) => {
1395 assert_eq!(rect.color.r, 69.0);
1396 assert_eq!(rect.color.g, 168.0);
1397 assert_eq!(rect.color.b, 90.0);
1398 assert_eq!(rect.color.a, 255.0);
1399 assert_eq!(rect.corner_radii.top_left, 9.0);
1400 assert_eq!(rect.corner_radii.top_right, 9.0);
1401 assert_eq!(rect.corner_radii.bottom_left, 9.0);
1402 assert_eq!(rect.corner_radii.bottom_right, 9.0);
1403 }
1404 _ => panic!("Expected Rectangle config for item 2"),
1405 }
1406
1407 assert_eq!(items[3].bounding_box.x, 275.99142);
1409 assert_eq!(items[3].bounding_box.y, 6.0);
1410 assert_eq!(items[3].bounding_box.width, 100.0);
1411 assert_eq!(items[3].bounding_box.height, 24.0);
1412 match &items[3].config {
1413 render_commands::RenderCommandConfig::Text(text) => {
1414 assert_eq!(text.text, "Wall");
1415 assert_eq!(text.color.r, 255.0);
1416 assert_eq!(text.color.g, 255.0);
1417 assert_eq!(text.color.b, 255.0);
1418 assert_eq!(text.color.a, 255.0);
1419 assert_eq!(text.font_size, 18);
1420 }
1421 _ => panic!("Expected Text config for item 3"),
1422 }
1423
1424 assert_eq!(items[4].bounding_box.x, 387.99142);
1426 assert_eq!(items[4].bounding_box.y, 9.0);
1427 assert_eq!(items[4].bounding_box.width, 200.0);
1428 assert_eq!(items[4].bounding_box.height, 18.0);
1429 match &items[4].config {
1430 render_commands::RenderCommandConfig::Rectangle(rect) => {
1431 assert_eq!(rect.color.r, 85.0);
1432 assert_eq!(rect.color.g, 85.0);
1433 assert_eq!(rect.color.b, 85.0);
1434 assert_eq!(rect.color.a, 255.0);
1435 assert_eq!(rect.corner_radii.top_left, 9.0);
1436 assert_eq!(rect.corner_radii.top_right, 9.0);
1437 assert_eq!(rect.corner_radii.bottom_left, 9.0);
1438 assert_eq!(rect.corner_radii.bottom_right, 9.0);
1439 }
1440 _ => panic!("Expected Rectangle config for item 4"),
1441 }
1442
1443 assert_eq!(items[5].bounding_box.x, 387.99142);
1445 assert_eq!(items[5].bounding_box.y, 9.0);
1446 assert_eq!(items[5].bounding_box.width, 200.0);
1447 assert_eq!(items[5].bounding_box.height, 18.0);
1448 match &items[5].config {
1449 render_commands::RenderCommandConfig::Rectangle(rect) => {
1450 assert_eq!(rect.color.r, 69.0);
1451 assert_eq!(rect.color.g, 168.0);
1452 assert_eq!(rect.color.b, 90.0);
1453 assert_eq!(rect.color.a, 255.0);
1454 assert_eq!(rect.corner_radii.top_left, 9.0);
1455 assert_eq!(rect.corner_radii.top_right, 9.0);
1456 assert_eq!(rect.corner_radii.bottom_left, 9.0);
1457 assert_eq!(rect.corner_radii.bottom_right, 9.0);
1458 }
1459 _ => panic!("Expected Rectangle config for item 5"),
1460 }
1461
1462 assert_eq!(items[6].bounding_box.x, 587.99146);
1464 assert_eq!(items[6].bounding_box.y, 6.0);
1465 assert_eq!(items[6].bounding_box.width, 100.0);
1466 assert_eq!(items[6].bounding_box.height, 24.0);
1467 match &items[6].config {
1468 render_commands::RenderCommandConfig::Text(text) => {
1469 assert_eq!(text.text, "Tower");
1470 assert_eq!(text.color.r, 255.0);
1471 assert_eq!(text.color.g, 255.0);
1472 assert_eq!(text.color.b, 255.0);
1473 assert_eq!(text.color.a, 255.0);
1474 assert_eq!(text.font_size, 18);
1475 }
1476 _ => panic!("Expected Text config for item 6"),
1477 }
1478
1479 assert_eq!(items[7].bounding_box.x, 699.99146);
1481 assert_eq!(items[7].bounding_box.y, 9.0);
1482 assert_eq!(items[7].bounding_box.width, 300.0);
1483 assert_eq!(items[7].bounding_box.height, 18.0);
1484 match &items[7].config {
1485 render_commands::RenderCommandConfig::Rectangle(rect) => {
1486 assert_eq!(rect.color.r, 85.0);
1487 assert_eq!(rect.color.g, 85.0);
1488 assert_eq!(rect.color.b, 85.0);
1489 assert_eq!(rect.color.a, 255.0);
1490 assert_eq!(rect.corner_radii.top_left, 9.0);
1491 assert_eq!(rect.corner_radii.top_right, 9.0);
1492 assert_eq!(rect.corner_radii.bottom_left, 9.0);
1493 assert_eq!(rect.corner_radii.bottom_right, 9.0);
1494 }
1495 _ => panic!("Expected Rectangle config for item 7"),
1496 }
1497
1498 assert_eq!(items[8].bounding_box.x, 699.99146);
1500 assert_eq!(items[8].bounding_box.y, 9.0);
1501 assert_eq!(items[8].bounding_box.width, 300.0);
1502 assert_eq!(items[8].bounding_box.height, 18.0);
1503 match &items[8].config {
1504 render_commands::RenderCommandConfig::Rectangle(rect) => {
1505 assert_eq!(rect.color.r, 69.0);
1506 assert_eq!(rect.color.g, 168.0);
1507 assert_eq!(rect.color.b, 90.0);
1508 assert_eq!(rect.color.a, 255.0);
1509 assert_eq!(rect.corner_radii.top_left, 9.0);
1510 assert_eq!(rect.corner_radii.top_right, 9.0);
1511 assert_eq!(rect.corner_radii.bottom_left, 9.0);
1512 assert_eq!(rect.corner_radii.bottom_right, 9.0);
1513 }
1514 _ => panic!("Expected Rectangle config for item 8"),
1515 }
1516 }
1517
1518 #[test]
1519 fn test_grow_weights_distribute_proportionally() {
1520 let mut ply = Ply::<()>::new_headless(Dimensions::new(600.0, 120.0));
1521 let mut ui = ply.begin();
1522
1523 ui.element()
1524 .width(fixed!(600.0))
1525 .height(fixed!(120.0))
1526 .layout(|l| l.direction(crate::layout::LayoutDirection::LeftToRight))
1527 .children(|ui| {
1528 ui.element()
1529 .width(grow!(0.0, f32::MAX, 1.0))
1530 .height(grow!())
1531 .background_color(0xFF0000)
1532 .empty();
1533
1534 ui.element()
1535 .width(grow!(0.0, f32::MAX, 2.0))
1536 .height(grow!())
1537 .background_color(0x00FF00)
1538 .empty();
1539 });
1540
1541 let items = ui.eval();
1542 assert_eq!(items.len(), 2);
1543 assert_eq!(items[0].bounding_box.width, 200.0);
1544 assert_eq!(items[1].bounding_box.width, 400.0);
1545 }
1546
1547 #[test]
1548 fn test_zero_weight_grow_behaves_like_fit() {
1549 let mut ply = Ply::<()>::new_headless(Dimensions::new(600.0, 120.0));
1550 let mut ui = ply.begin();
1551
1552 ui.element()
1553 .width(fixed!(600.0))
1554 .height(fixed!(120.0))
1555 .layout(|l| l.direction(crate::layout::LayoutDirection::LeftToRight))
1556 .children(|ui| {
1557 ui.element()
1558 .width(grow!(50.0, f32::MAX, 0.0))
1559 .height(grow!())
1560 .background_color(0xFF0000)
1561 .empty();
1562
1563 ui.element()
1564 .width(grow!())
1565 .height(grow!())
1566 .background_color(0x00FF00)
1567 .empty();
1568 });
1569
1570 let items = ui.eval();
1571 assert_eq!(items.len(), 2);
1572 assert_eq!(items[0].bounding_box.width, 50.0);
1573 assert_eq!(items[1].bounding_box.width, 550.0);
1574 }
1575
1576 #[test]
1577 fn test_single_main_axis_grow_respects_max() {
1578 let mut ply = Ply::<()>::new_headless(Dimensions::new(600.0, 120.0));
1579 let mut ui = ply.begin();
1580
1581 ui.element()
1582 .width(fixed!(600.0))
1583 .height(fixed!(120.0))
1584 .layout(|l| l.direction(crate::layout::LayoutDirection::LeftToRight))
1585 .children(|ui| {
1586 ui.element()
1587 .width(grow!(0.0, 300.0, 2.0))
1588 .height(grow!())
1589 .background_color(0xFF0000)
1590 .empty();
1591 });
1592
1593 let items = ui.eval();
1594 assert_eq!(items.len(), 1);
1595 assert_eq!(items[0].bounding_box.width, 300.0);
1596 }
1597
1598 #[rustfmt::skip]
1599 #[test]
1600 fn test_floating() {
1601 let mut ply = Ply::<()>::new_headless(Dimensions::new(1000.0, 1000.0));
1602
1603 let mut ui = ply.begin();
1604
1605 ui.set_measure_text_function(|_, _| {
1606 Dimensions::new(100.0, 24.0)
1607 });
1608
1609 ui.element().width(fixed!(20.0)).height(fixed!(20.0))
1610 .layout(|l| l.align(crate::align::AlignX::CenterX, crate::align::AlignY::CenterY))
1611 .floating(|f| f
1612 .attach_root()
1613 .anchor((crate::align::AlignX::CenterX, crate::align::AlignY::CenterY), (crate::align::AlignX::Left, crate::align::AlignY::Top))
1614 .offset((100.0, 150.0))
1615 .passthrough()
1616 .z_index(110)
1617 )
1618 .corner_radius(10.0)
1619 .background_color(0x4488DD)
1620 .children(|ui| {
1621 ui.text("Re", |t| t
1622 .font_size(6)
1623 .color(0xFFFFFF)
1624 );
1625 });
1626
1627 let items = ui.eval();
1628
1629 for item in &items {
1630 println!(
1631 "id: {}\nbbox: {:?}\nconfig: {:?}",
1632 item.id, item.bounding_box, item.config,
1633 );
1634 }
1635
1636 assert_eq!(items.len(), 2);
1637
1638 assert_eq!(items[0].bounding_box.x, 90.0);
1639 assert_eq!(items[0].bounding_box.y, 140.0);
1640 assert_eq!(items[0].bounding_box.width, 20.0);
1641 assert_eq!(items[0].bounding_box.height, 20.0);
1642 match &items[0].config {
1643 render_commands::RenderCommandConfig::Rectangle(rect) => {
1644 assert_eq!(rect.color.r, 68.0);
1645 assert_eq!(rect.color.g, 136.0);
1646 assert_eq!(rect.color.b, 221.0);
1647 assert_eq!(rect.color.a, 255.0);
1648 assert_eq!(rect.corner_radii.top_left, 10.0);
1649 assert_eq!(rect.corner_radii.top_right, 10.0);
1650 assert_eq!(rect.corner_radii.bottom_left, 10.0);
1651 assert_eq!(rect.corner_radii.bottom_right, 10.0);
1652 }
1653 _ => panic!("Expected Rectangle config for item 0"),
1654 }
1655
1656 assert_eq!(items[1].bounding_box.x, 50.0);
1657 assert_eq!(items[1].bounding_box.y, 138.0);
1658 assert_eq!(items[1].bounding_box.width, 100.0);
1659 assert_eq!(items[1].bounding_box.height, 24.0);
1660 match &items[1].config {
1661 render_commands::RenderCommandConfig::Text(text) => {
1662 assert_eq!(text.text, "Re");
1663 assert_eq!(text.color.r, 255.0);
1664 assert_eq!(text.color.g, 255.0);
1665 assert_eq!(text.color.b, 255.0);
1666 assert_eq!(text.color.a, 255.0);
1667 assert_eq!(text.font_size, 6);
1668 }
1669 _ => panic!("Expected Text config for item 1"),
1670 }
1671 }
1672
1673 #[rustfmt::skip]
1674 #[test]
1675 fn test_simple_text_measure() {
1676 let mut ply = Ply::<()>::new_headless(Dimensions::new(800.0, 600.0));
1677
1678 ply.set_measure_text_function(|_text, _config| {
1679 Dimensions::default()
1680 });
1681
1682 let mut ui = ply.begin();
1683
1684 ui.element()
1685 .id("parent_rect")
1686 .width(Sizing::Fixed(100.0))
1687 .height(Sizing::Fixed(100.0))
1688 .layout(|l| l
1689 .padding(Padding::all(10))
1690 )
1691 .background_color(Color::rgb(255., 255., 255.))
1692 .children(|ui| {
1693 ui.text(&format!("{}", 1234), |t| t
1694 .color(Color::rgb(255., 255., 255.))
1695 .font_size(24)
1696 );
1697 });
1698
1699 let _items = ui.eval();
1700 }
1701
1702 #[rustfmt::skip]
1703 #[test]
1704 fn test_shader_begin_end() {
1705 use shaders::ShaderAsset;
1706
1707 let test_shader = ShaderAsset::Source {
1708 file_name: "test_effect.glsl",
1709 fragment: "#version 100\nprecision lowp float;\nvarying vec2 uv;\nuniform sampler2D Texture;\nvoid main() { gl_FragColor = texture2D(Texture, uv); }",
1710 };
1711
1712 let mut ply = Ply::<()>::new_headless(Dimensions::new(800.0, 600.0));
1713 ply.set_measure_text_function(|_, _| Dimensions::new(100.0, 24.0));
1714
1715 let mut ui = ply.begin();
1716
1717 ui.element()
1719 .width(fixed!(200.0)).height(fixed!(200.0))
1720 .background_color(0xFF0000)
1721 .shader(&test_shader, |s| {
1722 s.uniform("time", 1.0f32);
1723 })
1724 .children(|ui| {
1725 ui.element()
1726 .width(fixed!(100.0)).height(fixed!(100.0))
1727 .background_color(0x00FF00)
1728 .empty();
1729 });
1730
1731 let items = ui.eval();
1732
1733 for (i, item) in items.iter().enumerate() {
1734 println!(
1735 "[{}] config: {:?}, bbox: {:?}",
1736 i, item.config, item.bounding_box,
1737 );
1738 }
1739
1740 assert!(items.len() >= 4, "Expected at least 4 items, got {}", items.len());
1746
1747 match &items[0].config {
1748 render_commands::RenderCommandConfig::GroupBegin { shader, visual_rotation } => {
1749 let config = shader.as_ref().expect("GroupBegin should have shader config");
1750 assert!(!config.fragment.is_empty(), "GroupBegin should have fragment source");
1751 assert_eq!(config.uniforms.len(), 1);
1752 assert_eq!(config.uniforms[0].name, "time");
1753 assert!(visual_rotation.is_none(), "Shader-only group should have no visual_rotation");
1754 }
1755 other => panic!("Expected GroupBegin for item 0, got {:?}", other),
1756 }
1757
1758 match &items[1].config {
1759 render_commands::RenderCommandConfig::Rectangle(rect) => {
1760 assert_eq!(rect.color.r, 255.0);
1761 assert_eq!(rect.color.g, 0.0);
1762 assert_eq!(rect.color.b, 0.0);
1763 }
1764 other => panic!("Expected Rectangle for item 1, got {:?}", other),
1765 }
1766
1767 match &items[2].config {
1768 render_commands::RenderCommandConfig::Rectangle(rect) => {
1769 assert_eq!(rect.color.r, 0.0);
1770 assert_eq!(rect.color.g, 255.0);
1771 assert_eq!(rect.color.b, 0.0);
1772 }
1773 other => panic!("Expected Rectangle for item 2, got {:?}", other),
1774 }
1775
1776 match &items[3].config {
1777 render_commands::RenderCommandConfig::GroupEnd => {}
1778 other => panic!("Expected GroupEnd for item 3, got {:?}", other),
1779 }
1780 }
1781
1782 #[rustfmt::skip]
1783 #[test]
1784 fn test_multiple_shaders_nested() {
1785 use shaders::ShaderAsset;
1786
1787 let shader_a = ShaderAsset::Source {
1788 file_name: "shader_a.glsl",
1789 fragment: "#version 100\nprecision lowp float;\nvoid main() { gl_FragColor = vec4(1.0); }",
1790 };
1791 let shader_b = ShaderAsset::Source {
1792 file_name: "shader_b.glsl",
1793 fragment: "#version 100\nprecision lowp float;\nvoid main() { gl_FragColor = vec4(0.5); }",
1794 };
1795
1796 let mut ply = Ply::<()>::new_headless(Dimensions::new(800.0, 600.0));
1797 ply.set_measure_text_function(|_, _| Dimensions::new(100.0, 24.0));
1798
1799 let mut ui = ply.begin();
1800
1801 ui.element()
1803 .width(fixed!(200.0)).height(fixed!(200.0))
1804 .background_color(0xFFFFFF)
1805 .shader(&shader_a, |s| { s.uniform("val", 1.0f32); })
1806 .shader(&shader_b, |s| { s.uniform("val", 2.0f32); })
1807 .children(|ui| {
1808 ui.element()
1809 .width(fixed!(50.0)).height(fixed!(50.0))
1810 .background_color(0x0000FF)
1811 .empty();
1812 });
1813
1814 let items = ui.eval();
1815
1816 for (i, item) in items.iter().enumerate() {
1817 println!("[{}] config: {:?}", i, item.config);
1818 }
1819
1820 assert!(items.len() >= 6, "Expected at least 6 items, got {}", items.len());
1828
1829 match &items[0].config {
1830 render_commands::RenderCommandConfig::GroupBegin { shader, .. } => {
1831 let config = shader.as_ref().unwrap();
1832 assert!(config.fragment.contains("0.5"), "Expected shader_b fragment");
1834 }
1835 other => panic!("Expected GroupBegin(shader_b) for item 0, got {:?}", other),
1836 }
1837 match &items[1].config {
1838 render_commands::RenderCommandConfig::GroupBegin { shader, .. } => {
1839 let config = shader.as_ref().unwrap();
1840 assert!(config.fragment.contains("1.0"), "Expected shader_a fragment");
1842 }
1843 other => panic!("Expected GroupBegin(shader_a) for item 1, got {:?}", other),
1844 }
1845 match &items[2].config {
1846 render_commands::RenderCommandConfig::Rectangle(_) => {}
1847 other => panic!("Expected Rectangle for item 2, got {:?}", other),
1848 }
1849 match &items[3].config {
1850 render_commands::RenderCommandConfig::Rectangle(_) => {}
1851 other => panic!("Expected Rectangle for item 3, got {:?}", other),
1852 }
1853 match &items[4].config {
1854 render_commands::RenderCommandConfig::GroupEnd => {}
1855 other => panic!("Expected GroupEnd for item 4, got {:?}", other),
1856 }
1857 match &items[5].config {
1858 render_commands::RenderCommandConfig::GroupEnd => {}
1859 other => panic!("Expected GroupEnd for item 5, got {:?}", other),
1860 }
1861 }
1862
1863 #[rustfmt::skip]
1864 #[test]
1865 fn test_effect_on_render_command() {
1866 use shaders::ShaderAsset;
1867
1868 let effect_shader = ShaderAsset::Source {
1869 file_name: "gradient.glsl",
1870 fragment: "#version 100\nprecision lowp float;\nvoid main() { gl_FragColor = vec4(1.0); }",
1871 };
1872
1873 let mut ply = Ply::<()>::new_headless(Dimensions::new(800.0, 600.0));
1874
1875 let mut ui = ply.begin();
1876
1877 ui.element()
1878 .width(fixed!(200.0)).height(fixed!(100.0))
1879 .background_color(0xFF0000)
1880 .effect(&effect_shader, |s| {
1881 s.uniform("color_a", [1.0f32, 0.0, 0.0, 1.0])
1882 .uniform("color_b", [0.0f32, 0.0, 1.0, 1.0]);
1883 })
1884 .empty();
1885
1886 let items = ui.eval();
1887
1888 assert_eq!(items.len(), 1, "Expected 1 item, got {}", items.len());
1889 assert_eq!(items[0].effects.len(), 1, "Expected 1 effect");
1890 assert_eq!(items[0].effects[0].uniforms.len(), 2);
1891 assert_eq!(items[0].effects[0].uniforms[0].name, "color_a");
1892 assert_eq!(items[0].effects[0].uniforms[1].name, "color_b");
1893 }
1894
1895 #[rustfmt::skip]
1896 #[test]
1897 fn test_visual_rotation_emits_group() {
1898 let mut ply = Ply::<()>::new_headless(Dimensions::new(800.0, 600.0));
1899 let mut ui = ply.begin();
1900
1901 ui.element()
1902 .width(fixed!(100.0)).height(fixed!(50.0))
1903 .background_color(0xFF0000)
1904 .rotate_visual(|r| r.degrees(45.0))
1905 .empty();
1906
1907 let items = ui.eval();
1908
1909 assert_eq!(items.len(), 3, "Expected 3 items, got {}", items.len());
1911
1912 match &items[0].config {
1913 render_commands::RenderCommandConfig::GroupBegin { shader, visual_rotation } => {
1914 assert!(shader.is_none(), "Rotation-only group should have no shader");
1915 let vr = visual_rotation.as_ref().expect("Should have visual_rotation");
1916 assert!((vr.rotation_radians - 45.0_f32.to_radians()).abs() < 0.001);
1917 assert_eq!(vr.pivot_x, 0.5);
1918 assert_eq!(vr.pivot_y, 0.5);
1919 assert!(!vr.flip_x);
1920 assert!(!vr.flip_y);
1921 }
1922 other => panic!("Expected GroupBegin for item 0, got {:?}", other),
1923 }
1924
1925 match &items[1].config {
1926 render_commands::RenderCommandConfig::Rectangle(_) => {}
1927 other => panic!("Expected Rectangle for item 1, got {:?}", other),
1928 }
1929
1930 match &items[2].config {
1931 render_commands::RenderCommandConfig::GroupEnd => {}
1932 other => panic!("Expected GroupEnd for item 2, got {:?}", other),
1933 }
1934 }
1935
1936 #[rustfmt::skip]
1937 #[test]
1938 fn test_visual_rotation_with_shader_merged() {
1939 use shaders::ShaderAsset;
1940
1941 let test_shader = ShaderAsset::Source {
1942 file_name: "merge_test.glsl",
1943 fragment: "#version 100\nprecision lowp float;\nvoid main() { gl_FragColor = vec4(1.0); }",
1944 };
1945
1946 let mut ply = Ply::<()>::new_headless(Dimensions::new(800.0, 600.0));
1947 let mut ui = ply.begin();
1948
1949 ui.element()
1951 .width(fixed!(100.0)).height(fixed!(100.0))
1952 .background_color(0xFF0000)
1953 .shader(&test_shader, |s| { s.uniform("v", 1.0f32); })
1954 .rotate_visual(|r| r.degrees(30.0).pivot((0.0, 0.0)))
1955 .empty();
1956
1957 let items = ui.eval();
1958
1959 assert_eq!(items.len(), 3, "Expected 3 items (merged), got {}", items.len());
1961
1962 match &items[0].config {
1963 render_commands::RenderCommandConfig::GroupBegin { shader, visual_rotation } => {
1964 assert!(shader.is_some(), "Merged group should have shader");
1965 let vr = visual_rotation.as_ref().expect("Merged group should have visual_rotation");
1966 assert!((vr.rotation_radians - 30.0_f32.to_radians()).abs() < 0.001);
1967 assert_eq!(vr.pivot_x, 0.0);
1968 assert_eq!(vr.pivot_y, 0.0);
1969 }
1970 other => panic!("Expected GroupBegin for item 0, got {:?}", other),
1971 }
1972 }
1973
1974 #[rustfmt::skip]
1975 #[test]
1976 fn test_visual_rotation_with_multiple_shaders() {
1977 use shaders::ShaderAsset;
1978
1979 let shader_a = ShaderAsset::Source {
1980 file_name: "vr_a.glsl",
1981 fragment: "#version 100\nprecision lowp float;\nvoid main() { gl_FragColor = vec4(1.0); }",
1982 };
1983 let shader_b = ShaderAsset::Source {
1984 file_name: "vr_b.glsl",
1985 fragment: "#version 100\nprecision lowp float;\nvoid main() { gl_FragColor = vec4(0.5); }",
1986 };
1987
1988 let mut ply = Ply::<()>::new_headless(Dimensions::new(800.0, 600.0));
1989 let mut ui = ply.begin();
1990
1991 ui.element()
1992 .width(fixed!(100.0)).height(fixed!(100.0))
1993 .background_color(0xFF0000)
1994 .shader(&shader_a, |s| { s.uniform("v", 1.0f32); })
1995 .shader(&shader_b, |s| { s.uniform("v", 2.0f32); })
1996 .rotate_visual(|r| r.degrees(90.0))
1997 .empty();
1998
1999 let items = ui.eval();
2000
2001 assert!(items.len() >= 5, "Expected at least 5 items, got {}", items.len());
2003
2004 match &items[0].config {
2006 render_commands::RenderCommandConfig::GroupBegin { shader, visual_rotation } => {
2007 assert!(shader.is_some(), "Outermost should have shader");
2008 assert!(visual_rotation.is_some(), "Outermost should have visual_rotation");
2009 }
2010 other => panic!("Expected GroupBegin for item 0, got {:?}", other),
2011 }
2012
2013 match &items[1].config {
2015 render_commands::RenderCommandConfig::GroupBegin { shader, visual_rotation } => {
2016 assert!(shader.is_some(), "Inner should have shader");
2017 assert!(visual_rotation.is_none(), "Inner should NOT have visual_rotation");
2018 }
2019 other => panic!("Expected GroupBegin for item 1, got {:?}", other),
2020 }
2021 }
2022
2023 #[rustfmt::skip]
2024 #[test]
2025 fn test_visual_rotation_noop_skipped() {
2026 let mut ply = Ply::<()>::new_headless(Dimensions::new(800.0, 600.0));
2027 let mut ui = ply.begin();
2028
2029 ui.element()
2031 .width(fixed!(100.0)).height(fixed!(100.0))
2032 .background_color(0xFF0000)
2033 .rotate_visual(|r| r.degrees(0.0))
2034 .empty();
2035
2036 let items = ui.eval();
2037
2038 assert_eq!(items.len(), 1, "Noop rotation should produce 1 item, got {}", items.len());
2040 match &items[0].config {
2041 render_commands::RenderCommandConfig::Rectangle(_) => {}
2042 other => panic!("Expected Rectangle, got {:?}", other),
2043 }
2044 }
2045
2046 #[rustfmt::skip]
2047 #[test]
2048 fn test_visual_rotation_flip_only() {
2049 let mut ply = Ply::<()>::new_headless(Dimensions::new(800.0, 600.0));
2050 let mut ui = ply.begin();
2051
2052 ui.element()
2054 .width(fixed!(100.0)).height(fixed!(100.0))
2055 .background_color(0xFF0000)
2056 .rotate_visual(|r| r.flip_x())
2057 .empty();
2058
2059 let items = ui.eval();
2060
2061 assert_eq!(items.len(), 3, "Flip-only should produce 3 items, got {}", items.len());
2063 match &items[0].config {
2064 render_commands::RenderCommandConfig::GroupBegin { visual_rotation, .. } => {
2065 let vr = visual_rotation.as_ref().expect("Should have rotation config");
2066 assert!(vr.flip_x);
2067 assert!(!vr.flip_y);
2068 assert_eq!(vr.rotation_radians, 0.0);
2069 }
2070 other => panic!("Expected GroupBegin, got {:?}", other),
2071 }
2072 }
2073
2074 #[rustfmt::skip]
2075 #[test]
2076 fn test_visual_rotation_preserves_bounding_box() {
2077 let mut ply = Ply::<()>::new_headless(Dimensions::new(800.0, 600.0));
2078 let mut ui = ply.begin();
2079
2080 ui.element()
2081 .width(fixed!(200.0)).height(fixed!(100.0))
2082 .background_color(0xFF0000)
2083 .rotate_visual(|r| r.degrees(45.0))
2084 .empty();
2085
2086 let items = ui.eval();
2087
2088 let rect = &items[1]; assert_eq!(rect.bounding_box.width, 200.0);
2091 assert_eq!(rect.bounding_box.height, 100.0);
2092 }
2093
2094 #[rustfmt::skip]
2095 #[test]
2096 fn test_visual_rotation_config_values() {
2097 let mut ply = Ply::<()>::new_headless(Dimensions::new(800.0, 600.0));
2098 let mut ui = ply.begin();
2099
2100 ui.element()
2101 .width(fixed!(100.0)).height(fixed!(100.0))
2102 .background_color(0xFF0000)
2103 .rotate_visual(|r| r
2104 .radians(std::f32::consts::FRAC_PI_2)
2105 .pivot((0.25, 0.75))
2106 .flip_x()
2107 .flip_y()
2108 )
2109 .empty();
2110
2111 let items = ui.eval();
2112
2113 match &items[0].config {
2114 render_commands::RenderCommandConfig::GroupBegin { visual_rotation, .. } => {
2115 let vr = visual_rotation.as_ref().unwrap();
2116 assert!((vr.rotation_radians - std::f32::consts::FRAC_PI_2).abs() < 0.001);
2117 assert_eq!(vr.pivot_x, 0.25);
2118 assert_eq!(vr.pivot_y, 0.75);
2119 assert!(vr.flip_x);
2120 assert!(vr.flip_y);
2121 }
2122 other => panic!("Expected GroupBegin, got {:?}", other),
2123 }
2124 }
2125
2126 #[rustfmt::skip]
2127 #[test]
2128 fn test_shape_rotation_emits_with_rotation() {
2129 let mut ply = Ply::<()>::new_headless(Dimensions::new(800.0, 600.0));
2130 let mut ui = ply.begin();
2131
2132 ui.element()
2133 .width(fixed!(100.0)).height(fixed!(50.0))
2134 .background_color(0xFF0000)
2135 .rotate_shape(|r| r.degrees(45.0))
2136 .empty();
2137
2138 let items = ui.eval();
2139
2140 assert_eq!(items.len(), 1, "Expected 1 item, got {}", items.len());
2142 let sr = items[0].shape_rotation.as_ref().expect("Should have shape_rotation");
2143 assert!((sr.rotation_radians - 45.0_f32.to_radians()).abs() < 0.001);
2144 assert!(!sr.flip_x);
2145 assert!(!sr.flip_y);
2146 }
2147
2148 #[rustfmt::skip]
2149 #[test]
2150 fn test_shape_rotation_aabb_90_degrees() {
2151 let mut ply = Ply::<()>::new_headless(Dimensions::new(800.0, 600.0));
2153 let mut ui = ply.begin();
2154
2155 ui.element().width(grow!()).height(grow!())
2156 .layout(|l| l)
2157 .children(|ui| {
2158 ui.element()
2159 .width(fixed!(200.0)).height(fixed!(100.0))
2160 .background_color(0xFF0000)
2161 .rotate_shape(|r| r.degrees(90.0))
2162 .empty();
2163 });
2164
2165 let items = ui.eval();
2166
2167 let rect = items.iter().find(|i| matches!(i.config, render_commands::RenderCommandConfig::Rectangle(_))).unwrap();
2169 assert!((rect.bounding_box.width - 200.0).abs() < 0.1, "width should be 200, got {}", rect.bounding_box.width);
2171 assert!((rect.bounding_box.height - 100.0).abs() < 0.1, "height should be 100, got {}", rect.bounding_box.height);
2172 }
2173
2174 #[rustfmt::skip]
2175 #[test]
2176 fn test_shape_rotation_aabb_45_degrees_sharp() {
2177 let mut ply = Ply::<()>::new_headless(Dimensions::new(800.0, 600.0));
2179 let mut ui = ply.begin();
2180
2181 ui.element().width(grow!()).height(grow!())
2183 .layout(|l| l.direction(layout::LayoutDirection::LeftToRight))
2184 .children(|ui| {
2185 ui.element()
2186 .width(fixed!(100.0)).height(fixed!(100.0))
2187 .background_color(0xFF0000)
2188 .rotate_shape(|r| r.degrees(45.0))
2189 .empty();
2190
2191 ui.element()
2193 .width(fixed!(50.0)).height(fixed!(50.0))
2194 .background_color(0x00FF00)
2195 .empty();
2196 });
2197
2198 let items = ui.eval();
2199
2200 let rects: Vec<_> = items.iter()
2202 .filter(|i| matches!(i.config, render_commands::RenderCommandConfig::Rectangle(_)))
2203 .collect();
2204 assert!(rects.len() >= 2, "Expected at least 2 rectangles, got {}", rects.len());
2205
2206 let expected_aabb_w = (2.0_f32.sqrt()) * 100.0; let green_x = rects[1].bounding_box.x;
2208 assert!((green_x - expected_aabb_w).abs() < 1.0,
2210 "Green rect x should be ~{}, got {}", expected_aabb_w, green_x);
2211 }
2212
2213 #[rustfmt::skip]
2214 #[test]
2215 fn test_shape_rotation_aabb_45_degrees_rounded() {
2216 let mut ply = Ply::<()>::new_headless(Dimensions::new(800.0, 600.0));
2219 let mut ui = ply.begin();
2220
2221 ui.element().width(grow!()).height(grow!())
2222 .layout(|l| l.direction(layout::LayoutDirection::LeftToRight))
2223 .children(|ui| {
2224 ui.element()
2225 .width(fixed!(100.0)).height(fixed!(100.0))
2226 .corner_radius(10.0)
2227 .background_color(0xFF0000)
2228 .rotate_shape(|r| r.degrees(45.0))
2229 .empty();
2230
2231 ui.element()
2232 .width(fixed!(50.0)).height(fixed!(50.0))
2233 .background_color(0x00FF00)
2234 .empty();
2235 });
2236
2237 let items = ui.eval();
2238
2239 let rects: Vec<_> = items.iter()
2240 .filter(|i| matches!(i.config, render_commands::RenderCommandConfig::Rectangle(_)))
2241 .collect();
2242 assert!(rects.len() >= 2);
2243
2244 let expected_aabb_w = 80.0 * 2.0_f32.sqrt() + 20.0;
2246 let green_x = rects[1].bounding_box.x;
2247 assert!((green_x - expected_aabb_w).abs() < 1.0,
2249 "Green rect x should be ~{}, got {}", expected_aabb_w, green_x);
2250 }
2251
2252 #[rustfmt::skip]
2253 #[test]
2254 fn test_shape_rotation_noop_no_aabb_change() {
2255 let mut ply = Ply::<()>::new_headless(Dimensions::new(800.0, 600.0));
2257 let mut ui = ply.begin();
2258
2259 ui.element()
2260 .width(fixed!(100.0)).height(fixed!(50.0))
2261 .background_color(0xFF0000)
2262 .rotate_shape(|r| r.degrees(0.0))
2263 .empty();
2264
2265 let items = ui.eval();
2266 assert_eq!(items.len(), 1);
2267 assert_eq!(items[0].bounding_box.width, 100.0);
2268 assert_eq!(items[0].bounding_box.height, 50.0);
2269 assert!(items[0].shape_rotation.is_none(), "Noop shape rotation should be filtered");
2272 }
2273
2274 #[rustfmt::skip]
2275 #[test]
2276 fn test_shape_rotation_flip_only() {
2277 let mut ply = Ply::<()>::new_headless(Dimensions::new(800.0, 600.0));
2279 let mut ui = ply.begin();
2280
2281 ui.element()
2282 .width(fixed!(100.0)).height(fixed!(50.0))
2283 .background_color(0xFF0000)
2284 .rotate_shape(|r| r.flip_x())
2285 .empty();
2286
2287 let items = ui.eval();
2288 assert_eq!(items.len(), 1);
2289 let sr = items[0].shape_rotation.as_ref().expect("flip_x should produce shape_rotation");
2290 assert!(sr.flip_x);
2291 assert!(!sr.flip_y);
2292 assert_eq!(items[0].bounding_box.width, 100.0);
2294 assert_eq!(items[0].bounding_box.height, 50.0);
2295 }
2296
2297 #[rustfmt::skip]
2298 #[test]
2299 fn test_shape_rotation_180_no_aabb_change() {
2300 let mut ply = Ply::<()>::new_headless(Dimensions::new(800.0, 600.0));
2302 let mut ui = ply.begin();
2303
2304 ui.element()
2305 .width(fixed!(200.0)).height(fixed!(100.0))
2306 .background_color(0xFF0000)
2307 .rotate_shape(|r| r.degrees(180.0))
2308 .empty();
2309
2310 let items = ui.eval();
2311 assert_eq!(items.len(), 1);
2312 assert_eq!(items[0].bounding_box.width, 200.0);
2313 assert_eq!(items[0].bounding_box.height, 100.0);
2314 }
2315
2316 #[test]
2317 fn test_classify_angle() {
2318 use math::{classify_angle, AngleType};
2319 assert_eq!(classify_angle(0.0), AngleType::Zero);
2320 assert_eq!(classify_angle(std::f32::consts::TAU), AngleType::Zero);
2321 assert_eq!(classify_angle(-std::f32::consts::TAU), AngleType::Zero);
2322 assert_eq!(classify_angle(std::f32::consts::FRAC_PI_2), AngleType::Right90);
2323 assert_eq!(classify_angle(std::f32::consts::PI), AngleType::Straight180);
2324 assert_eq!(classify_angle(3.0 * std::f32::consts::FRAC_PI_2), AngleType::Right270);
2325 match classify_angle(1.0) {
2326 AngleType::Arbitrary(v) => assert!((v - 1.0).abs() < 0.01),
2327 other => panic!("Expected Arbitrary, got {:?}", other),
2328 }
2329 }
2330
2331 #[test]
2332 fn test_compute_rotated_aabb_zero() {
2333 use math::compute_rotated_aabb;
2334 use layout::CornerRadius;
2335 let cr = CornerRadius::default();
2336 let (w, h) = compute_rotated_aabb(100.0, 50.0, &cr, 0.0);
2337 assert_eq!(w, 100.0);
2338 assert_eq!(h, 50.0);
2339 }
2340
2341 #[test]
2342 fn test_compute_rotated_aabb_90() {
2343 use math::compute_rotated_aabb;
2344 use layout::CornerRadius;
2345 let cr = CornerRadius::default();
2346 let (w, h) = compute_rotated_aabb(200.0, 100.0, &cr, std::f32::consts::FRAC_PI_2);
2347 assert!((w - 100.0).abs() < 0.1, "w should be 100, got {}", w);
2348 assert!((h - 200.0).abs() < 0.1, "h should be 200, got {}", h);
2349 }
2350
2351 #[test]
2352 fn test_compute_rotated_aabb_45_sharp() {
2353 use math::compute_rotated_aabb;
2354 use layout::CornerRadius;
2355 let cr = CornerRadius::default();
2356 let theta = std::f32::consts::FRAC_PI_4;
2357 let (w, h) = compute_rotated_aabb(100.0, 100.0, &cr, theta);
2358 let expected = 100.0 * 2.0_f32.sqrt();
2359 assert!((w - expected).abs() < 0.5, "w should be ~{}, got {}", expected, w);
2360 assert!((h - expected).abs() < 0.5, "h should be ~{}, got {}", expected, h);
2361 }
2362
2363 #[test]
2364 fn test_compute_rotated_aabb_45_rounded() {
2365 use math::compute_rotated_aabb;
2366 use layout::CornerRadius;
2367 let cr = CornerRadius { top_left: 10.0, top_right: 10.0, bottom_left: 10.0, bottom_right: 10.0 };
2368 let theta = std::f32::consts::FRAC_PI_4;
2369 let (w, h) = compute_rotated_aabb(100.0, 100.0, &cr, theta);
2370 let expected = 80.0 * 2.0_f32.sqrt() + 20.0; assert!((w - expected).abs() < 0.5, "w should be ~{}, got {}", expected, w);
2372 assert!((h - expected).abs() < 0.5, "h should be ~{}, got {}", expected, h);
2373 }
2374
2375 #[test]
2376 fn test_on_press_callback_fires() {
2377 use std::cell::RefCell;
2378 use std::rc::Rc;
2379
2380 let mut ply = Ply::<()>::new_headless(Dimensions::new(800.0, 600.0));
2381 let press_count = Rc::new(RefCell::new(0u32));
2382 let release_count = Rc::new(RefCell::new(0u32));
2383
2384 {
2386 let mut ui = ply.begin();
2387 ui.element()
2388 .id("btn")
2389 .width(fixed!(100.0))
2390 .height(fixed!(100.0))
2391 .empty();
2392 ui.eval();
2393 }
2394
2395 {
2397 let pc = press_count.clone();
2398 let rc = release_count.clone();
2399 let mut ui = ply.begin();
2400 ui.element()
2401 .id("btn")
2402 .width(fixed!(100.0))
2403 .height(fixed!(100.0))
2404 .on_press(move |_, _| { *pc.borrow_mut() += 1; })
2405 .on_release(move |_, _| { *rc.borrow_mut() += 1; })
2406 .empty();
2407 ui.eval();
2408 }
2409
2410 ply.context.set_pointer_state(Vector2::new(50.0, 50.0), true);
2412 assert_eq!(*press_count.borrow(), 1, "on_press should fire once");
2413 assert_eq!(*release_count.borrow(), 0, "on_release should not fire yet");
2414
2415 ply.context.set_pointer_state(Vector2::new(50.0, 50.0), false);
2417 assert_eq!(*release_count.borrow(), 1, "on_release should fire once");
2418 }
2419
2420 #[test]
2421 fn test_pressed_query() {
2422 let mut ply = Ply::<()>::new_headless(Dimensions::new(800.0, 600.0));
2423
2424 {
2426 let mut ui = ply.begin();
2427 ui.element()
2428 .id("btn")
2429 .width(fixed!(100.0))
2430 .height(fixed!(100.0))
2431 .empty();
2432 ui.eval();
2433 }
2434
2435 ply.context.set_pointer_state(Vector2::new(50.0, 50.0), true);
2437
2438 {
2440 let mut ui = ply.begin();
2441 ui.element()
2442 .id("btn")
2443 .width(fixed!(100.0))
2444 .height(fixed!(100.0))
2445 .children(|ui| {
2446 assert!(ui.pressed(), "element should report as pressed");
2447 });
2448 ui.eval();
2449 }
2450 }
2451
2452 #[test]
2453 fn test_just_pressed_query_one_frame() {
2454 let mut ply = Ply::<()>::new_headless(Dimensions::new(800.0, 600.0));
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 .empty();
2464 ui.eval();
2465 }
2466
2467 ply.context.set_pointer_state(Vector2::new(50.0, 50.0), true);
2469
2470 {
2472 let mut ui = ply.begin();
2473 ui.element()
2474 .id("btn")
2475 .width(fixed!(100.0))
2476 .height(fixed!(100.0))
2477 .children(|ui| {
2478 assert!(ui.just_pressed(), "element should report as just pressed");
2479 assert!(
2480 ui.is_just_pressed("btn"),
2481 "ID query should report just pressed"
2482 );
2483 assert!(ui.pressed(), "element should still be pressed");
2484 });
2485 ui.eval();
2486 }
2487
2488 ply.context.set_pointer_state(Vector2::new(50.0, 50.0), true);
2490
2491 {
2493 let mut ui = ply.begin();
2494 ui.element()
2495 .id("btn")
2496 .width(fixed!(100.0))
2497 .height(fixed!(100.0))
2498 .children(|ui| {
2499 assert!(!ui.just_pressed(), "just pressed should clear next frame");
2500 assert!(
2501 !ui.is_just_pressed("btn"),
2502 "ID just pressed should clear next frame"
2503 );
2504 assert!(ui.pressed(), "element should remain pressed while held");
2505 });
2506 ui.eval();
2507 }
2508 }
2509
2510 #[test]
2511 fn test_keyboard_activation_marks_just_pressed() {
2512 let mut ply = Ply::<()>::new_headless(Dimensions::new(800.0, 600.0));
2513
2514 {
2516 let mut ui = ply.begin();
2517 ui.element()
2518 .id("btn")
2519 .width(fixed!(100.0))
2520 .height(fixed!(100.0))
2521 .empty();
2522 ui.eval();
2523 }
2524
2525 ply.set_focus("btn");
2527 ply.context.handle_keyboard_activation(true, false);
2528 assert!(ply.is_pressed("btn"), "keyboard press should set pressed state");
2529
2530 {
2532 let mut ui = ply.begin();
2533 ui.element()
2534 .id("btn")
2535 .width(fixed!(100.0))
2536 .height(fixed!(100.0))
2537 .children(|ui| {
2538 assert!(
2539 ui.just_pressed(),
2540 "keyboard activation should mark just_pressed"
2541 );
2542 assert!(
2543 ui.is_just_pressed("btn"),
2544 "ID query should include keyboard press"
2545 );
2546 assert!(ui.pressed(), "element should be pressed while key is held");
2547 });
2548 ui.eval();
2549 }
2550
2551 {
2553 let mut ui = ply.begin();
2554 ui.element()
2555 .id("btn")
2556 .width(fixed!(100.0))
2557 .height(fixed!(100.0))
2558 .children(|ui| {
2559 assert!(!ui.just_pressed());
2560 assert!(!ui.is_just_pressed("btn"));
2561 assert!(ui.pressed());
2562 });
2563 ui.eval();
2564 }
2565
2566 ply.context.handle_keyboard_activation(false, true);
2568 assert!(!ply.is_pressed("btn"), "keyboard release should clear pressed state");
2569 }
2570
2571 #[test]
2572 fn test_just_released_query_one_frame() {
2573 let mut ply = Ply::<()>::new_headless(Dimensions::new(800.0, 600.0));
2574
2575 {
2577 let mut ui = ply.begin();
2578 ui.element()
2579 .id("btn")
2580 .width(fixed!(100.0))
2581 .height(fixed!(100.0))
2582 .empty();
2583 ui.eval();
2584 }
2585
2586 ply.context.set_pointer_state(Vector2::new(50.0, 50.0), true);
2588 ply.context.set_pointer_state(Vector2::new(50.0, 50.0), false);
2589
2590 {
2592 let mut ui = ply.begin();
2593 ui.element()
2594 .id("btn")
2595 .width(fixed!(100.0))
2596 .height(fixed!(100.0))
2597 .children(|ui| {
2598 assert!(ui.just_released(), "element should report as just released");
2599 assert!(
2600 ui.is_just_released("btn"),
2601 "ID query should report just released"
2602 );
2603 assert!(!ui.pressed(), "element should no longer be pressed");
2604 });
2605 ui.eval();
2606 }
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_released(), "just released should clear next frame");
2617 assert!(
2618 !ui.is_just_released("btn"),
2619 "ID query should clear next frame"
2620 );
2621 });
2622 ui.eval();
2623 }
2624 }
2625
2626 #[test]
2627 fn test_keyboard_activation_marks_just_released() {
2628 let mut ply = Ply::<()>::new_headless(Dimensions::new(800.0, 600.0));
2629
2630 {
2632 let mut ui = ply.begin();
2633 ui.element()
2634 .id("btn")
2635 .width(fixed!(100.0))
2636 .height(fixed!(100.0))
2637 .empty();
2638 ui.eval();
2639 }
2640
2641 ply.set_focus("btn");
2643 ply.context.handle_keyboard_activation(true, false);
2644 assert!(ply.is_pressed("btn"), "keyboard press should set pressed state");
2645
2646 ply.context.handle_keyboard_activation(false, true);
2647 assert!(
2648 !ply.is_pressed("btn"),
2649 "keyboard release should clear pressed state"
2650 );
2651
2652 {
2654 let mut ui = ply.begin();
2655 ui.element()
2656 .id("btn")
2657 .width(fixed!(100.0))
2658 .height(fixed!(100.0))
2659 .children(|ui| {
2660 assert!(
2661 ui.just_released(),
2662 "keyboard release should mark just_released"
2663 );
2664 assert!(
2665 ui.is_just_released("btn"),
2666 "ID query should include keyboard release"
2667 );
2668 });
2669 ui.eval();
2670 }
2671
2672 {
2674 let mut ui = ply.begin();
2675 ui.element()
2676 .id("btn")
2677 .width(fixed!(100.0))
2678 .height(fixed!(100.0))
2679 .children(|ui| {
2680 assert!(!ui.just_released());
2681 assert!(!ui.is_just_released("btn"));
2682 });
2683 ui.eval();
2684 }
2685 }
2686
2687 #[test]
2688 fn test_tab_navigation_cycles_focus() {
2689 let mut ply = Ply::<()>::new_headless(Dimensions::new(800.0, 600.0));
2690
2691 {
2693 let mut ui = ply.begin();
2694 ui.element()
2695 .id("a")
2696 .width(fixed!(100.0))
2697 .height(fixed!(50.0))
2698 .accessibility(|a| a.button("A"))
2699 .empty();
2700 ui.element()
2701 .id("b")
2702 .width(fixed!(100.0))
2703 .height(fixed!(50.0))
2704 .accessibility(|a| a.button("B"))
2705 .empty();
2706 ui.element()
2707 .id("c")
2708 .width(fixed!(100.0))
2709 .height(fixed!(50.0))
2710 .accessibility(|a| a.button("C"))
2711 .empty();
2712 ui.eval();
2713 }
2714
2715 let id_a = Id::from("a").id;
2716 let id_b = Id::from("b").id;
2717 let id_c = Id::from("c").id;
2718
2719 assert_eq!(ply.focused_element(), None);
2721
2722 ply.context.cycle_focus(false);
2724 assert_eq!(ply.context.focused_element_id, id_a);
2725
2726 ply.context.cycle_focus(false);
2728 assert_eq!(ply.context.focused_element_id, id_b);
2729
2730 ply.context.cycle_focus(false);
2732 assert_eq!(ply.context.focused_element_id, id_c);
2733
2734 ply.context.cycle_focus(false);
2736 assert_eq!(ply.context.focused_element_id, id_a);
2737
2738 ply.context.cycle_focus(true);
2740 assert_eq!(ply.context.focused_element_id, id_c);
2741 }
2742
2743 #[test]
2744 fn test_tab_index_ordering() {
2745 let mut ply = Ply::<()>::new_headless(Dimensions::new(800.0, 600.0));
2746
2747 {
2749 let mut ui = ply.begin();
2750 ui.element()
2751 .id("third")
2752 .width(fixed!(100.0))
2753 .height(fixed!(50.0))
2754 .accessibility(|a| a.button("Third").tab_index(3))
2755 .empty();
2756 ui.element()
2757 .id("first")
2758 .width(fixed!(100.0))
2759 .height(fixed!(50.0))
2760 .accessibility(|a| a.button("First").tab_index(1))
2761 .empty();
2762 ui.element()
2763 .id("second")
2764 .width(fixed!(100.0))
2765 .height(fixed!(50.0))
2766 .accessibility(|a| a.button("Second").tab_index(2))
2767 .empty();
2768 ui.eval();
2769 }
2770
2771 let id_first = Id::from("first").id;
2772 let id_second = Id::from("second").id;
2773 let id_third = Id::from("third").id;
2774
2775 ply.context.cycle_focus(false);
2777 assert_eq!(ply.context.focused_element_id, id_first);
2778 ply.context.cycle_focus(false);
2779 assert_eq!(ply.context.focused_element_id, id_second);
2780 ply.context.cycle_focus(false);
2781 assert_eq!(ply.context.focused_element_id, id_third);
2782 }
2783
2784 #[test]
2785 fn test_arrow_key_navigation() {
2786 let mut ply = Ply::<()>::new_headless(Dimensions::new(800.0, 600.0));
2787 use engine::ArrowDirection;
2788
2789 let id_a = Id::from("a").id;
2790 let id_b = Id::from("b").id;
2791
2792 {
2794 let mut ui = ply.begin();
2795 ui.element()
2796 .id("a")
2797 .width(fixed!(100.0))
2798 .height(fixed!(50.0))
2799 .accessibility(|a| a.button("A").focus_right("b"))
2800 .empty();
2801 ui.element()
2802 .id("b")
2803 .width(fixed!(100.0))
2804 .height(fixed!(50.0))
2805 .accessibility(|a| a.button("B").focus_left("a"))
2806 .empty();
2807 ui.eval();
2808 }
2809
2810 ply.context.set_focus(id_a);
2812 assert_eq!(ply.context.focused_element_id, id_a);
2813
2814 ply.context.arrow_focus(ArrowDirection::Right);
2816 assert_eq!(ply.context.focused_element_id, id_b);
2817
2818 ply.context.arrow_focus(ArrowDirection::Left);
2820 assert_eq!(ply.context.focused_element_id, id_a);
2821
2822 ply.context.arrow_focus(ArrowDirection::Up);
2824 assert_eq!(ply.context.focused_element_id, id_a);
2825 }
2826
2827 #[test]
2828 fn test_focused_query() {
2829 let mut ply = Ply::<()>::new_headless(Dimensions::new(800.0, 600.0));
2830
2831 let id_a = Id::from("a").id;
2832
2833 {
2835 let mut ui = ply.begin();
2836 ui.element()
2837 .id("a")
2838 .width(fixed!(100.0))
2839 .height(fixed!(50.0))
2840 .accessibility(|a| a.button("A"))
2841 .empty();
2842 ui.eval();
2843 }
2844
2845 ply.context.set_focus(id_a);
2846
2847 {
2849 let mut ui = ply.begin();
2850 ui.element()
2851 .id("a")
2852 .width(fixed!(100.0))
2853 .height(fixed!(50.0))
2854 .accessibility(|a| a.button("A"))
2855 .children(|ui| {
2856 assert!(ui.focused(), "element should report as focused");
2857 });
2858 ui.eval();
2859 }
2860 }
2861
2862 #[test]
2863 fn test_on_focus_callback_fires_on_tab() {
2864 use std::cell::RefCell;
2865 use std::rc::Rc;
2866
2867 let mut ply = Ply::<()>::new_headless(Dimensions::new(800.0, 600.0));
2868 let focus_a = Rc::new(RefCell::new(0u32));
2869 let unfocus_a = Rc::new(RefCell::new(0u32));
2870 let focus_b = Rc::new(RefCell::new(0u32));
2871
2872 {
2874 let fa = focus_a.clone();
2875 let ua = unfocus_a.clone();
2876 let fb = focus_b.clone();
2877 let mut ui = ply.begin();
2878 ui.element()
2879 .id("a")
2880 .width(fixed!(100.0))
2881 .height(fixed!(50.0))
2882 .accessibility(|a| a.button("A"))
2883 .on_focus(move |_| { *fa.borrow_mut() += 1; })
2884 .on_unfocus(move |_| { *ua.borrow_mut() += 1; })
2885 .empty();
2886 ui.element()
2887 .id("b")
2888 .width(fixed!(100.0))
2889 .height(fixed!(50.0))
2890 .accessibility(|a| a.button("B"))
2891 .on_focus(move |_| { *fb.borrow_mut() += 1; })
2892 .empty();
2893 ui.eval();
2894 }
2895
2896 ply.context.cycle_focus(false);
2898 assert_eq!(*focus_a.borrow(), 1, "on_focus should fire for A");
2899 assert_eq!(*unfocus_a.borrow(), 0, "on_unfocus should not fire yet");
2900
2901 ply.context.cycle_focus(false);
2903 assert_eq!(*unfocus_a.borrow(), 1, "on_unfocus should fire for A");
2904 assert_eq!(*focus_b.borrow(), 1, "on_focus should fire for B");
2905 }
2906
2907 #[test]
2908 fn test_on_focus_callback_fires_on_set_focus() {
2909 use std::cell::RefCell;
2910 use std::rc::Rc;
2911
2912 let mut ply = Ply::<()>::new_headless(Dimensions::new(800.0, 600.0));
2913 let focus_count = Rc::new(RefCell::new(0u32));
2914 let unfocus_count = Rc::new(RefCell::new(0u32));
2915
2916 let id_a = Id::from("a").id;
2917
2918 {
2920 let fc = focus_count.clone();
2921 let uc = unfocus_count.clone();
2922 let mut ui = ply.begin();
2923 ui.element()
2924 .id("a")
2925 .width(fixed!(100.0))
2926 .height(fixed!(50.0))
2927 .accessibility(|a| a.button("A"))
2928 .on_focus(move |_| { *fc.borrow_mut() += 1; })
2929 .on_unfocus(move |_| { *uc.borrow_mut() += 1; })
2930 .empty();
2931 ui.eval();
2932 }
2933
2934 ply.context.set_focus(id_a);
2936 assert_eq!(*focus_count.borrow(), 1, "on_focus should fire on set_focus");
2937
2938 ply.context.clear_focus();
2940 assert_eq!(*unfocus_count.borrow(), 1, "on_unfocus should fire on clear_focus");
2941 }
2942
2943 #[test]
2944 fn test_focus_ring_render_command() {
2945 use render_commands::RenderCommandConfig;
2946
2947 let mut ply = Ply::<()>::new_headless(Dimensions::new(800.0, 600.0));
2948 let id_a = Id::from("a").id;
2949
2950 {
2952 let mut ui = ply.begin();
2953 ui.element()
2954 .id("a")
2955 .width(fixed!(100.0))
2956 .height(fixed!(50.0))
2957 .corner_radius(8.0)
2958 .accessibility(|a| a.button("A"))
2959 .empty();
2960 ui.eval();
2961 }
2962
2963 ply.context.focus_from_keyboard = true;
2965 ply.context.set_focus(id_a);
2966
2967 {
2969 let mut ui = ply.begin();
2970 ui.element()
2971 .id("a")
2972 .width(fixed!(100.0))
2973 .height(fixed!(50.0))
2974 .corner_radius(8.0)
2975 .accessibility(|a| a.button("A"))
2976 .empty();
2977 let items = ui.eval();
2978
2979 let focus_ring = items.iter().find(|cmd| {
2981 cmd.z_index == 32764 && matches!(cmd.config, RenderCommandConfig::Border(_))
2982 });
2983 assert!(focus_ring.is_some(), "Focus ring border should be in render commands");
2984
2985 let ring = focus_ring.unwrap();
2986 assert!(ring.bounding_box.width > 100.0, "Focus ring should be wider than element");
2988 assert!(ring.bounding_box.height > 50.0, "Focus ring should be taller than element");
2989 }
2990 }
2991
2992 #[test]
2993 fn test_overflow_scrollbar_renders_and_moves_with_set_scroll_position() {
2994 let mut ply = Ply::<()>::new_headless(Dimensions::new(400.0, 300.0));
2995
2996 {
2997 let mut ui = ply.begin();
2998 ui.element()
2999 .id("scroll")
3000 .width(fixed!(100.0))
3001 .height(fixed!(100.0))
3002 .overflow(|o| o.scroll().scrollbar(|s| s))
3003 .children(|ui| {
3004 ui.element()
3005 .width(fixed!(300.0))
3006 .height(fixed!(250.0))
3007 .empty();
3008 });
3009 let items = ui.eval();
3010
3011 let rects: Vec<_> = items
3012 .iter()
3013 .filter(|cmd| matches!(cmd.config, render_commands::RenderCommandConfig::Rectangle(_)))
3014 .collect();
3015 assert!(rects.len() >= 2, "Expected scrollbar thumb rectangles");
3016
3017 let v_thumb = rects
3018 .iter()
3019 .find(|cmd| (cmd.bounding_box.width - 6.0).abs() < 0.1 && cmd.bounding_box.height > cmd.bounding_box.width)
3020 .expect("Expected vertical scrollbar thumb");
3021 let h_thumb = rects
3022 .iter()
3023 .find(|cmd| (cmd.bounding_box.height - 6.0).abs() < 0.1 && cmd.bounding_box.width > cmd.bounding_box.height)
3024 .expect("Expected horizontal scrollbar thumb");
3025
3026 assert!((v_thumb.bounding_box.x - 94.0).abs() < 0.1);
3027 assert!((h_thumb.bounding_box.y - 94.0).abs() < 0.1);
3028 }
3029
3030 ply.set_scroll_position("scroll", (80.0, 90.0));
3031
3032 {
3033 let mut ui = ply.begin();
3034 ui.element()
3035 .id("scroll")
3036 .width(fixed!(100.0))
3037 .height(fixed!(100.0))
3038 .overflow(|o| o.scroll().scrollbar(|s| s))
3039 .children(|ui| {
3040 ui.element()
3041 .width(fixed!(300.0))
3042 .height(fixed!(250.0))
3043 .empty();
3044 });
3045 let items = ui.eval();
3046
3047 let rects: Vec<_> = items
3048 .iter()
3049 .filter(|cmd| matches!(cmd.config, render_commands::RenderCommandConfig::Rectangle(_)))
3050 .collect();
3051
3052 let v_thumb = rects
3053 .iter()
3054 .find(|cmd| (cmd.bounding_box.width - 6.0).abs() < 0.1 && cmd.bounding_box.height > cmd.bounding_box.width)
3055 .expect("Expected vertical scrollbar thumb");
3056 let h_thumb = rects
3057 .iter()
3058 .find(|cmd| (cmd.bounding_box.height - 6.0).abs() < 0.1 && cmd.bounding_box.width > cmd.bounding_box.height)
3059 .expect("Expected horizontal scrollbar thumb");
3060
3061 assert!(v_thumb.bounding_box.y > 0.0);
3062 assert!(h_thumb.bounding_box.x > 0.0);
3063 }
3064 }
3065}