1use bevy::prelude::*;
2use bevy_egui::egui;
3
4#[derive(Resource)]
6pub struct ResponsiveInfo {
7 pub screen_size: Vec2,
8 pub device_type: DeviceType,
9 pub orientation: Orientation,
10 pub scale_factor: f32,
11}
12
13impl Default for ResponsiveInfo {
14 fn default() -> Self {
15 let mut info = Self {
17 screen_size: Vec2::new(1024.0, 768.0),
18 device_type: DeviceType::Desktop,
19 orientation: Orientation::Landscape,
20 scale_factor: 1.0,
21 };
22 info.update(info.screen_size, info.scale_factor);
24
25 info
26 }
27}
28
29#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
31pub enum DeviceType {
32 #[default]
33 Desktop,
34 Tablet,
35 Mobile,
36}
37
38#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
40pub enum Orientation {
41 #[default]
42 Landscape,
43 Portrait,
44}
45
46pub struct Breakpoints;
48
49impl Breakpoints {
50 pub const MOBILE_MAX: f32 = 480.0;
51 pub const TABLET_MAX: f32 = 768.0;
52 pub const DESKTOP_MIN: f32 = 769.0;
53}
54
55#[derive(Debug, Clone, Copy, PartialEq, Eq)]
57pub enum ResponsiveFontSize {
58 Small,
59 Medium,
60 Large,
61 Header,
62 Title,
63}
64
65#[derive(Debug, Clone, Copy, PartialEq, Eq)]
67pub enum ResponsiveSpacing {
68 XSmall,
69 Small,
70 Medium,
71 Large,
72 XLarge,
73}
74
75#[derive(Debug, Clone, Copy, PartialEq, Eq)]
77pub enum ResponsiveBorderRadius {
78 None,
79 Small,
80 Medium,
81 Large,
82 XLarge,
83}
84
85#[derive(Debug, Clone, Copy, PartialEq, Eq)]
87pub enum ResponsiveMargin {
88 None,
89 Small,
90 Medium,
91 Large,
92 XLarge,
93}
94
95impl ResponsiveInfo {
96 pub fn new() -> Self {
97 Self::default()
98 }
99
100 pub fn update(&mut self, window_size: Vec2, scale_factor: f32) {
102 self.screen_size = window_size;
103 self.scale_factor = scale_factor;
104
105 let min_dimension = window_size.x.min(window_size.y);
107 self.device_type = if min_dimension <= Breakpoints::MOBILE_MAX {
108 DeviceType::Mobile
109 } else if min_dimension <= Breakpoints::TABLET_MAX {
110 DeviceType::Tablet
111 } else {
112 DeviceType::Desktop
113 };
114
115 self.orientation = if window_size.x > window_size.y {
117 Orientation::Landscape
118 } else {
119 Orientation::Portrait
120 };
121 }
122
123 pub fn font_size(&self, size_type: ResponsiveFontSize) -> f32 {
125 let base_scale = match (self.device_type, self.orientation) {
126 (DeviceType::Mobile, Orientation::Landscape) => 0.7,
127 (DeviceType::Mobile, Orientation::Portrait) => 0.8,
128 (DeviceType::Tablet, _) => 0.9,
129 (DeviceType::Desktop, _) => 1.0,
130 };
131
132 let base_size = match size_type {
133 ResponsiveFontSize::Small => 12.0,
134 ResponsiveFontSize::Medium => 16.0,
135 ResponsiveFontSize::Large => 20.0,
136 ResponsiveFontSize::Header => 24.0,
137 ResponsiveFontSize::Title => 32.0,
138 };
139
140 base_size * base_scale
141 }
142
143 pub fn spacing(&self, spacing_type: ResponsiveSpacing) -> f32 {
145 let base_scale = match (self.device_type, self.orientation) {
146 (DeviceType::Mobile, Orientation::Landscape) => 0.6,
147 (DeviceType::Mobile, Orientation::Portrait) => 0.7,
148 (DeviceType::Tablet, _) => 0.85,
149 (DeviceType::Desktop, _) => 1.0,
150 };
151
152 let base_spacing = match spacing_type {
153 ResponsiveSpacing::XSmall => 4.0,
154 ResponsiveSpacing::Small => 8.0,
155 ResponsiveSpacing::Medium => 16.0,
156 ResponsiveSpacing::Large => 24.0,
157 ResponsiveSpacing::XLarge => 32.0,
158 };
159
160 base_spacing * base_scale
161 }
162
163 pub fn border_radius(&self, radius_type: ResponsiveBorderRadius) -> f32 {
165 let base_scale = match (self.device_type, self.orientation) {
166 (DeviceType::Mobile, Orientation::Landscape) => 0.8,
167 (DeviceType::Mobile, Orientation::Portrait) => 0.9,
168 (DeviceType::Tablet, _) => 0.95,
169 (DeviceType::Desktop, _) => 1.0,
170 };
171
172 let base_radius = match radius_type {
173 ResponsiveBorderRadius::None => 0.0,
174 ResponsiveBorderRadius::Small => 4.0,
175 ResponsiveBorderRadius::Medium => 8.0,
176 ResponsiveBorderRadius::Large => 12.0,
177 ResponsiveBorderRadius::XLarge => 16.0,
178 };
179
180 base_radius * base_scale
181 }
182
183 pub fn margin(&self, margin_type: ResponsiveMargin) -> i8 {
185 let base_scale = match (self.device_type, self.orientation) {
186 (DeviceType::Mobile, Orientation::Landscape) => 0.5,
187 (DeviceType::Mobile, Orientation::Portrait) => 0.6,
188 (DeviceType::Tablet, _) => 0.8,
189 (DeviceType::Desktop, _) => 1.0,
190 };
191
192 let base_margin = match margin_type {
193 ResponsiveMargin::None => 0.0,
194 ResponsiveMargin::Small => 8.0,
195 ResponsiveMargin::Medium => 16.0,
196 ResponsiveMargin::Large => 24.0,
197 ResponsiveMargin::XLarge => 32.0,
198 };
199
200 (base_margin * base_scale) as i8
201 }
202
203 pub fn container_margin(&self) -> i8 {
205 self.margin(ResponsiveMargin::Medium)
206 }
207
208 pub fn margin_all(&self, margin_type: ResponsiveMargin) -> egui::Margin {
210 let margin = self.margin(margin_type);
211 egui::Margin::same(margin)
212 }
213
214 pub fn container_margin_egui(&self) -> egui::Margin {
216 self.margin_all(ResponsiveMargin::Medium)
217 }
218
219 pub fn margin_symmetric(
221 &self,
222 horizontal: ResponsiveMargin,
223 vertical: ResponsiveMargin,
224 ) -> egui::Margin {
225 let h_margin = self.margin(horizontal);
226 let v_margin = self.margin(vertical);
227 egui::Margin::symmetric(h_margin, v_margin)
228 }
229
230 pub fn margin_custom(
232 &self,
233 left: ResponsiveMargin,
234 right: ResponsiveMargin,
235 top: ResponsiveMargin,
236 bottom: ResponsiveMargin,
237 ) -> egui::Margin {
238 egui::Margin {
239 left: self.margin(left),
240 right: self.margin(right),
241 top: self.margin(top),
242 bottom: self.margin(bottom),
243 }
244 }
245
246 pub fn is_mobile(&self) -> bool {
248 self.device_type == DeviceType::Mobile
249 }
250
251 pub fn is_tablet(&self) -> bool {
253 self.device_type == DeviceType::Tablet
254 }
255
256 pub fn is_desktop(&self) -> bool {
258 self.device_type == DeviceType::Desktop
259 }
260
261 pub fn is_portrait(&self) -> bool {
263 self.orientation == Orientation::Portrait
264 }
265}
266
267pub fn update_responsive_info(
269 mut responsive_info: ResMut<ResponsiveInfo>,
270 windows: Query<&Window>,
271) {
272 if let Ok(window) = windows.single() {
273 let window_size = Vec2::new(window.width(), window.height());
274 responsive_info.update(window_size, window.scale_factor());
275 }
276}
277
278pub struct ResponsivePlugin;
280
281impl Plugin for ResponsivePlugin {
282 fn build(&self, app: &mut App) {
283 app.init_resource::<ResponsiveInfo>()
284 .add_systems(PreUpdate, update_responsive_info);
285 }
286}