1use frus_core::{Alignment, Curve, Rect, Scene};
5use frus_layout::Style;
6
7use crate::interaction::Status;
8use crate::theme::Theme;
9use crate::widget::Widget;
10
11pub struct Transform<Msg = crate::callback::Callback> {
33 dx: f32,
34 dy: f32,
35 scale: Option<(f32, f32, Alignment)>,
37 rotate: Option<(f32, Alignment)>,
39 anim: Option<(f32, Curve)>,
42 children: Vec<Box<dyn Widget<Msg>>>,
43}
44
45impl<Msg> Transform<Msg> {
46 pub fn translate(dx: f32, dy: f32) -> Self {
49 Self {
50 dx,
51 dy,
52 scale: None,
53 rotate: None,
54 anim: None,
55 children: Vec::new(),
56 }
57 }
58
59 pub fn scale(factor: f32) -> Self {
62 Self::scale_xy_from(factor, factor, Alignment::CENTER)
63 }
64
65 pub fn scale_xy(sx: f32, sy: f32) -> Self {
69 Self::scale_xy_from(sx, sy, Alignment::CENTER)
70 }
71
72 pub fn scale_from(factor: f32, pivot: Alignment) -> Self {
75 Self::scale_xy_from(factor, factor, pivot)
76 }
77
78 pub fn scale_xy_from(sx: f32, sy: f32, pivot: Alignment) -> Self {
80 Self {
81 dx: 0.0,
82 dy: 0.0,
83 scale: Some((sx, sy, pivot)),
84 rotate: None,
85 anim: None,
86 children: Vec::new(),
87 }
88 }
89
90 pub fn rotate(radians: f32) -> Self {
93 Self::rotate_from(radians, Alignment::CENTER)
94 }
95
96 pub fn rotate_from(radians: f32, pivot: Alignment) -> Self {
98 Self {
99 dx: 0.0,
100 dy: 0.0,
101 scale: None,
102 rotate: Some((radians, pivot)),
103 anim: None,
104 children: Vec::new(),
105 }
106 }
107
108 pub fn and_translate(mut self, dx: f32, dy: f32) -> Self {
111 self.dx = dx;
112 self.dy = dy;
113 self
114 }
115
116 pub fn and_scale(self, factor: f32) -> Self {
119 self.and_scale_xy(factor, factor)
120 }
121
122 pub fn and_scale_xy(mut self, sx: f32, sy: f32) -> Self {
124 self.scale = Some((sx, sy, Alignment::CENTER));
125 self
126 }
127
128 pub fn and_rotate(mut self, radians: f32) -> Self {
131 self.rotate = Some((radians, Alignment::CENTER));
132 self
133 }
134
135 #[must_use]
156 pub fn animated(mut self, duration: f32, curve: Curve) -> Self {
157 self.anim = Some((duration, curve));
158 self
159 }
160
161 pub fn child(mut self, child: impl Widget<Msg> + 'static) -> Self {
163 self.children.clear();
164 self.children.push(Box::new(child));
165 self
166 }
167}
168
169impl<Msg: Clone> Widget<Msg> for Transform<Msg> {
170 fn style(&self) -> Style {
171 Style::default()
174 }
175
176 fn children(&self) -> &[Box<dyn Widget<Msg>>] {
177 &self.children
178 }
179
180 fn paint(&self, _bounds: Rect, _status: Status, _theme: &Theme, _scene: &mut Scene) {
181 }
183
184 fn on_click(&self) -> Option<Msg> {
185 None
186 }
187
188 fn transform_translate(&self) -> Option<(f32, f32)> {
189 ((self.dx != 0.0) || (self.dy != 0.0)).then_some((self.dx, self.dy))
190 }
191
192 fn transform_scale(&self) -> Option<(f32, f32, Alignment)> {
193 self.scale
194 }
195
196 fn transform_rotate(&self) -> Option<(f32, Alignment)> {
197 self.rotate
198 }
199
200 fn anim_transform(&self) -> Option<crate::runtime::TransformValues> {
205 self.anim.as_ref().map(|_| {
206 let (scale_x, scale_y) = self.scale.map_or((1.0, 1.0), |(x, y, _)| (x, y));
207 crate::runtime::TransformValues {
208 scale_x,
209 scale_y,
210 rotation: self.rotate.map_or(0.0, |(a, _)| a),
211 }
212 })
213 }
214
215 fn anim_duration(&self) -> f32 {
216 self.anim.as_ref().map_or(0.0, |(d, _)| *d)
217 }
218
219 fn anim_curve(&self) -> Curve {
220 self.anim.as_ref().map_or(Curve::Linear, |(_, c)| c.clone())
221 }
222}
223
224pub struct FractionalTranslation<Msg = crate::callback::Callback> {
247 fx: f32,
248 fy: f32,
249 anim: Option<(f32, Curve)>,
251 children: Vec<Box<dyn Widget<Msg>>>,
252}
253
254impl<Msg> FractionalTranslation<Msg> {
255 pub fn new(fx: f32, fy: f32) -> Self {
257 Self {
258 fx,
259 fy,
260 anim: None,
261 children: Vec::new(),
262 }
263 }
264
265 pub fn animated(mut self, duration: f32, curve: Curve) -> Self {
268 self.anim = Some((duration, curve));
269 self
270 }
271
272 pub fn child(mut self, child: impl Widget<Msg> + 'static) -> Self {
274 self.children.clear();
275 self.children.push(Box::new(child));
276 self
277 }
278}
279
280impl<Msg: Clone> Widget<Msg> for FractionalTranslation<Msg> {
281 fn style(&self) -> Style {
282 Style::default()
283 }
284
285 fn children(&self) -> &[Box<dyn Widget<Msg>>] {
286 &self.children
287 }
288
289 fn paint(&self, _bounds: Rect, _status: Status, _theme: &Theme, _scene: &mut Scene) {}
290
291 fn on_click(&self) -> Option<Msg> {
292 None
293 }
294
295 fn translate_fraction(&self) -> Option<(f32, f32)> {
296 Some((self.fx, self.fy))
297 }
298
299 fn anim_offset(&self) -> Option<(f32, f32)> {
300 self.anim.as_ref().map(|_| (self.fx, self.fy))
301 }
302
303 fn anim_duration(&self) -> f32 {
304 self.anim.as_ref().map_or(0.0, |(d, _)| *d)
305 }
306
307 fn anim_curve(&self) -> Curve {
308 self.anim.as_ref().map_or(Curve::Linear, |(_, c)| c.clone())
309 }
310
311 fn debug_name(&self) -> &'static str {
312 "FractionalTranslation"
313 }
314}
315
316#[cfg(test)]
317mod tests {
318 use super::*;
319 use crate::Container;
320 use frus_core::{Color, Primitive, Size};
321
322 #[test]
325 fn translate_offsets_the_child_at_paint() {
326 let red = Color::rgb(1.0, 0.0, 0.0);
327 let root = crate::Flex::<()>::column().width(100.0).child(
328 Transform::translate(30.0, 10.0)
329 .child(Container::new().width(20.0).height(20.0).color(red)),
330 );
331 let rt = crate::runtime::Runtime::default();
332 let theme = crate::Theme::dark();
333 let ui = crate::ui::build_ui(&root, Size::new(100.0, 200.0), &rt, &theme);
334 let rect = ui
335 .scene()
336 .primitives()
337 .iter()
338 .find_map(|p| match p {
339 Primitive::Rect { rect, color, .. } if color.r > 0.5 => Some(*rect),
340 _ => None,
341 })
342 .expect("the child's red background");
343 assert!(
344 (rect.x - 30.0).abs() < 0.5 && (rect.y - 10.0).abs() < 0.5,
345 "offset to (30, 10): {rect:?}"
346 );
347 }
348
349 #[test]
354 fn translate_does_not_affect_layout() {
355 let red = Color::rgb(1.0, 0.0, 0.0);
356 let green = Color::rgb(0.0, 1.0, 0.0);
357 let root = crate::Flex::<()>::column()
358 .width(100.0)
359 .child(
360 Transform::translate(0.0, 50.0)
361 .child(Container::new().flex(1.0).height(20.0).color(red)),
362 )
363 .child(Container::new().height(20.0).color(green));
364 let rt = crate::runtime::Runtime::default();
365 let theme = crate::Theme::dark();
366 let ui = crate::ui::build_ui(&root, Size::new(100.0, 200.0), &rt, &theme);
367 let green_y = ui
368 .scene()
369 .primitives()
370 .iter()
371 .find_map(|p| match p {
372 Primitive::Rect { rect, color, .. } if color.g > 0.5 && color.r < 0.5 => {
373 Some(rect.y)
374 }
375 _ => None,
376 })
377 .expect("the 2nd child's green background");
378 assert!(
381 (green_y - 20.0).abs() < 0.5,
382 "sibling in its layout place: y = {green_y}"
383 );
384 }
385
386 fn layer_affine<Msg: Clone>(ui: &crate::ui::Ui<Msg>) -> frus_core::Affine {
389 ui.scene()
390 .primitives()
391 .iter()
392 .find_map(|p| match p {
393 Primitive::Layer {
394 transform: Some(t), ..
395 } => Some(t.affine),
396 _ => None,
397 })
398 .expect("a transformed layer")
399 }
400
401 #[test]
404 fn scale_grows_the_child_about_its_center() {
405 use frus_core::Point;
406 let red = Color::rgb(1.0, 0.0, 0.0);
407 let root = crate::Flex::<()>::column().width(100.0).child(
408 Transform::scale(2.0).child(Container::new().width(20.0).height(20.0).color(red)),
409 );
410 let rt = crate::runtime::Runtime::default();
411 let theme = crate::Theme::dark();
412 let ui = crate::ui::build_ui(&root, Size::new(100.0, 200.0), &rt, &theme);
413 let m = layer_affine(&ui);
414 assert!(
415 (m.m[0] - 2.0).abs() < 1e-3 && (m.m[3] - 2.0).abs() < 1e-3,
416 "×2 : {:?}",
417 m.m
418 );
419 let c = m.apply(Point::new(10.0, 10.0));
420 assert!(
421 (c.x - 10.0).abs() < 0.5 && (c.y - 10.0).abs() < 0.5,
422 "centre fixe : {c:?}"
423 );
424 }
425
426 #[test]
429 fn scale_from_pins_the_pivot_corner() {
430 use frus_core::{Alignment, Point};
431 let red = Color::rgb(1.0, 0.0, 0.0);
432 let root = crate::Flex::<()>::column().width(100.0).child(
433 Transform::scale_from(2.0, Alignment::TOP_LEFT)
434 .child(Container::new().width(20.0).height(20.0).color(red)),
435 );
436 let rt = crate::runtime::Runtime::default();
437 let theme = crate::Theme::dark();
438 let ui = crate::ui::build_ui(&root, Size::new(100.0, 200.0), &rt, &theme);
439 let m = layer_affine(&ui);
440 assert!(
441 (m.m[0] - 2.0).abs() < 1e-3 && (m.m[3] - 2.0).abs() < 1e-3,
442 "×2 : {:?}",
443 m.m
444 );
445 let c = m.apply(Point::new(0.0, 0.0));
446 assert!(
447 c.x.abs() < 0.5 && c.y.abs() < 0.5,
448 "top-left corner unmoved: {c:?}"
449 );
450 }
451
452 #[test]
455 fn scale_xy_stretches_per_axis() {
456 use frus_core::Point;
457 let red = Color::rgb(1.0, 0.0, 0.0);
458 let root = crate::Flex::<()>::column().width(200.0).child(
459 Transform::scale_xy(3.0, 1.0)
460 .child(Container::new().width(20.0).height(20.0).color(red)),
461 );
462 let rt = crate::runtime::Runtime::default();
463 let theme = crate::Theme::dark();
464 let ui = crate::ui::build_ui(&root, Size::new(200.0, 200.0), &rt, &theme);
465 let m = layer_affine(&ui);
466 assert!(
467 (m.m[0] - 3.0).abs() < 1e-3 && (m.m[3] - 1.0).abs() < 1e-3,
468 "×3 en x, ×1 en y : {:?}",
469 m.m
470 );
471 let c = m.apply(Point::new(10.0, 10.0));
472 assert!(
473 (c.x - 10.0).abs() < 0.5 && (c.y - 10.0).abs() < 0.5,
474 "centre fixe : {c:?}"
475 );
476 }
477
478 #[test]
481 fn rotate_emits_a_rotated_layer() {
482 use frus_core::Point;
483 use std::f32::consts::FRAC_PI_2;
484 let red = Color::rgb(1.0, 0.0, 0.0);
485 let root = crate::Flex::<()>::column().width(100.0).child(
486 Transform::rotate(FRAC_PI_2)
487 .child(Container::new().width(40.0).height(20.0).color(red)),
488 );
489 let rt = crate::runtime::Runtime::default();
490 let theme = crate::Theme::dark();
491 let ui = crate::ui::build_ui(&root, Size::new(100.0, 200.0), &rt, &theme);
492 let m = layer_affine(&ui);
493 assert!(
494 m.m[0].abs() < 1e-3
495 && (m.m[1] - 1.0).abs() < 1e-3
496 && (m.m[2] + 1.0).abs() < 1e-3
497 && m.m[3].abs() < 1e-3,
498 "rotation +90° : {:?}",
499 m.m
500 );
501 let c = m.apply(Point::new(20.0, 10.0));
502 assert!(
503 (c.x - 20.0).abs() < 0.5 && (c.y - 10.0).abs() < 0.5,
504 "centre fixe : {c:?}"
505 );
506 }
507
508 #[test]
512 fn scale_and_rotate_compose() {
513 use std::f32::consts::FRAC_PI_2;
514 let red = Color::rgb(1.0, 0.0, 0.0);
515 let root = crate::Flex::<()>::column().width(100.0).child(
516 Transform::scale(2.0)
517 .and_rotate(FRAC_PI_2)
518 .child(Container::new().width(20.0).height(20.0).color(red)),
519 );
520 let rt = crate::runtime::Runtime::default();
521 let theme = crate::Theme::dark();
522 let ui = crate::ui::build_ui(&root, Size::new(100.0, 200.0), &rt, &theme);
523 let m = layer_affine(&ui);
524 assert!(
525 m.m[0].abs() < 1e-3
526 && (m.m[1] - 2.0).abs() < 1e-3
527 && (m.m[2] + 2.0).abs() < 1e-3
528 && m.m[3].abs() < 1e-3,
529 "rotation ∘ scale: {:?}",
530 m.m
531 );
532 }
533
534 #[test]
538 fn axis_aligned_transform_scales_the_focus_rect() {
539 use frus_core::Point;
540 let rt = crate::runtime::Runtime::default();
541 let theme = crate::Theme::dark();
542 let flat_ui = crate::ui::build_ui(
543 &crate::Flex::<i32>::column()
544 .width(200.0)
545 .child(crate::Button::new("Ok").on_press(1)),
546 Size::new(200.0, 200.0),
547 &rt,
548 &theme,
549 );
550 let flat = flat_ui
551 .focus_hit(Point::new(2.0, 2.0))
552 .expect("a focusable button")
553 .1;
554 let cy = flat.y + flat.height / 2.0;
555 let probe = Point::new(flat.x + flat.width + 2.0, cy);
557 assert!(
558 flat_ui.focus_hit(probe).is_none(),
559 "outside the flat button"
560 );
561
562 let scaled_ui = crate::ui::build_ui(
563 &crate::Flex::<i32>::column()
564 .width(200.0)
565 .child(Transform::scale(2.0).child(crate::Button::new("Ok").on_press(1))),
566 Size::new(200.0, 200.0),
567 &rt,
568 &theme,
569 );
570 let (_, r) = scaled_ui
571 .focus_hit(probe)
572 .expect("the button's enlarged image covers the point");
573 assert!(
574 (r.width - flat.width * 2.0).abs() < 1.0,
575 "rectangle de focus ~2× : {} vs {}",
576 r.width,
577 flat.width * 2.0
578 );
579 }
580
581 #[test]
586 fn rotate_hit_test_counter_rotates_the_point() {
587 use frus_core::Point;
588 use std::f32::consts::FRAC_PI_2;
589 let red = Color::rgb(1.0, 0.0, 0.0);
590 let root = crate::Flex::<i32>::column().width(100.0).child(
591 Transform::rotate(FRAC_PI_2).child(
592 Container::new()
593 .width(40.0)
594 .height(20.0)
595 .color(red)
596 .on_click(7),
597 ),
598 );
599 let rt = crate::runtime::Runtime::default();
600 let theme = crate::Theme::dark();
601 let ui = crate::ui::build_ui(&root, Size::new(100.0, 200.0), &rt, &theme);
602 assert!(
604 ui.hit(Point::new(20.0, 25.0)).is_some(),
605 "a click on the rotated position"
606 );
607 assert!(
609 ui.hit(Point::new(35.0, 10.0)).is_none(),
610 "l'ancienne position rate"
611 );
612 }
613
614 #[test]
617 fn a_fractional_translation_offsets_by_the_child_s_own_size() {
618 let red = Color::rgb(1.0, 0.0, 0.0);
619 let root = crate::Flex::<()>::column().width(100.0).child(
620 FractionalTranslation::new(0.5, 1.0)
621 .child(Container::new().width(20.0).height(20.0).color(red)),
622 );
623 let rt = crate::runtime::Runtime::default();
624 let theme = crate::Theme::dark();
625 let ui = crate::ui::build_ui(&root, Size::new(100.0, 200.0), &rt, &theme);
626 let rect = ui
627 .scene()
628 .primitives()
629 .iter()
630 .find_map(|p| match p {
631 Primitive::Rect { rect, color, .. } if color.r > 0.5 => Some(*rect),
632 _ => None,
633 })
634 .expect("the child's red background");
635 assert!(
636 (rect.x - 10.0).abs() < 0.5 && (rect.y - 20.0).abs() < 0.5,
637 "half a width across, a height down: {rect:?}"
638 );
639 }
640
641 #[test]
644 fn a_fractional_translation_moves_the_input_too() {
645 let root = crate::Flex::<i32>::column().width(100.0).child(
646 FractionalTranslation::new(1.0, 0.0).child(
647 Container::<i32>::new()
648 .width(20.0)
649 .height(20.0)
650 .color(Color::rgb(1.0, 0.0, 0.0))
651 .on_click(7),
652 ),
653 );
654 let rt = crate::runtime::Runtime::default();
655 let theme = crate::Theme::dark();
656 let ui = crate::ui::build_ui(&root, Size::new(100.0, 200.0), &rt, &theme);
657 let at = |x: f32| {
658 ui.hit(frus_core::Point::new(x, 10.0))
659 .and_then(|id| ui.msg_for(id))
660 };
661 assert_eq!(at(30.0), Some(7), "where it is painted");
662 assert_eq!(at(10.0), None, "not where it was laid out");
663 }
664
665 #[test]
668 fn a_fractional_translation_moves_nothing_else() {
669 let blue = Color::rgb(0.0, 0.0, 1.0);
670 let root = crate::Flex::<()>::column()
671 .width(100.0)
672 .child(
673 FractionalTranslation::new(0.0, 2.0).child(
674 Container::new()
675 .width(20.0)
676 .height(20.0)
677 .color(Color::rgb(1.0, 0.0, 0.0)),
678 ),
679 )
680 .child(Container::new().width(20.0).height(20.0).color(blue));
681 let rt = crate::runtime::Runtime::default();
682 let theme = crate::Theme::dark();
683 let ui = crate::ui::build_ui(&root, Size::new(100.0, 200.0), &rt, &theme);
684 let sibling = ui
685 .scene()
686 .primitives()
687 .iter()
688 .find_map(|p| match p {
689 Primitive::Rect { rect, color, .. } if color.b > 0.5 => Some(*rect),
690 _ => None,
691 })
692 .expect("the sibling");
693 assert!(
694 (sibling.y - 20.0).abs() < 0.5,
695 "the sibling stayed where the layout put it: {sibling:?}"
696 );
697 }
698}