armas_basic/layout/aspect_ratio.rs
1use egui::{Response, Ui, Vec2};
2
3/// Maintain aspect ratio wrapper
4///
5/// Constrains content to a specific aspect ratio.
6/// Inspired by `SwiftUI`'s aspectRatio modifier.
7///
8/// # Example
9///
10/// ```rust,no_run
11/// # use egui::Ui;
12/// # fn example(ui: &mut Ui) {
13/// use armas_basic::layout::AspectRatio;
14///
15/// // 16:9 aspect ratio
16/// AspectRatio::new(16.0 / 9.0)
17/// .show(ui, |ui| {
18/// ui.label("16:9 content");
19/// });
20///
21/// // Square (1:1)
22/// AspectRatio::square()
23/// .show(ui, |ui| {
24/// ui.label("Square content");
25/// });
26/// # }
27/// ```
28pub struct AspectRatio {
29 ratio: f32,
30 content_mode: ContentMode,
31}
32
33/// How content should be sized within the aspect ratio container
34#[derive(Debug, Clone, Copy, PartialEq, Eq)]
35pub enum ContentMode {
36 /// Content fills available space (may be cropped)
37 Fill,
38 /// Content fits within available space (may have empty space)
39 Fit,
40}
41
42impl AspectRatio {
43 /// Create aspect ratio constraint (width / height)
44 #[must_use]
45 pub const fn new(ratio: f32) -> Self {
46 Self {
47 ratio,
48 content_mode: ContentMode::Fit,
49 }
50 }
51
52 /// Create a square aspect ratio (1:1)
53 #[must_use]
54 pub const fn square() -> Self {
55 Self::new(1.0)
56 }
57
58 /// Create 16:9 aspect ratio (widescreen)
59 #[must_use]
60 pub fn widescreen() -> Self {
61 Self::new(16.0 / 9.0)
62 }
63
64 /// Create 4:3 aspect ratio (standard)
65 #[must_use]
66 pub fn standard() -> Self {
67 Self::new(4.0 / 3.0)
68 }
69
70 /// Set content mode (fill or fit)
71 #[must_use]
72 pub const fn content_mode(mut self, mode: ContentMode) -> Self {
73 self.content_mode = mode;
74 self
75 }
76
77 /// Show the aspect ratio container with the given content
78 pub fn show<R>(self, ui: &mut Ui, content: impl FnOnce(&mut Ui) -> R) -> Response {
79 let available = ui.available_size();
80
81 // Calculate size maintaining aspect ratio (ratio = width / height)
82 let size = match self.content_mode {
83 ContentMode::Fit => {
84 // If available height is 0 or very small, use width to calculate both dimensions
85 if available.y < 1.0 {
86 let height = available.x / self.ratio;
87 Vec2::new(available.x, height)
88 } else {
89 // Calculate what height we'd get if we use full width
90 let height_from_width = available.x / self.ratio;
91 // Calculate what width we'd get if we use full height
92 let width_from_height = available.y * self.ratio;
93
94 if height_from_width <= available.y {
95 // Width is the limiting factor - use full width
96 Vec2::new(available.x, height_from_width)
97 } else {
98 // Height is the limiting factor - use full height
99 Vec2::new(width_from_height, available.y)
100 }
101 }
102 }
103 ContentMode::Fill => {
104 // If available height is 0 or very small, use width to calculate both dimensions
105 if available.y < 1.0 {
106 let height = available.x / self.ratio;
107 Vec2::new(available.x, height)
108 } else {
109 let height_from_width = available.x / self.ratio;
110 let width_from_height = available.y * self.ratio;
111
112 if height_from_width >= available.y {
113 // Width gives us more - use it
114 Vec2::new(available.x, height_from_width)
115 } else {
116 // Height gives us more - use it
117 Vec2::new(width_from_height, available.y)
118 }
119 }
120 }
121 };
122
123 let (rect, response) = ui.allocate_exact_size(size, egui::Sense::hover());
124
125 ui.scope_builder(egui::UiBuilder::new().max_rect(rect), |ui| content(ui));
126
127 response
128 }
129}
130
131impl Default for AspectRatio {
132 fn default() -> Self {
133 Self::new(1.0)
134 }
135}