konnektoren_bevy/ui/
responsive.rs

1use bevy::prelude::*;
2use bevy_egui::egui;
3
4/// Resource that tracks the current screen size and provides responsive utilities
5#[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        // Initialize with reasonable defaults for desktop
16        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        // Update device type based on default screen size
23        info.update(info.screen_size, info.scale_factor);
24
25        info
26    }
27}
28
29/// Device type based on screen dimensions
30#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
31pub enum DeviceType {
32    #[default]
33    Desktop,
34    Tablet,
35    Mobile,
36}
37
38/// Screen orientation
39#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
40pub enum Orientation {
41    #[default]
42    Landscape,
43    Portrait,
44}
45
46/// Responsive breakpoints (in logical pixels)
47pub 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/// Responsive font size types
56#[derive(Debug, Clone, Copy, PartialEq, Eq)]
57pub enum ResponsiveFontSize {
58    Small,
59    Medium,
60    Large,
61    Header,
62    Title,
63}
64
65/// Responsive spacing types
66#[derive(Debug, Clone, Copy, PartialEq, Eq)]
67pub enum ResponsiveSpacing {
68    XSmall,
69    Small,
70    Medium,
71    Large,
72    XLarge,
73}
74
75/// Responsive border radius types
76#[derive(Debug, Clone, Copy, PartialEq, Eq)]
77pub enum ResponsiveBorderRadius {
78    None,
79    Small,
80    Medium,
81    Large,
82    XLarge,
83}
84
85/// Responsive margin types
86#[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    /// Update responsive info based on window size
101    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        // Determine device type based on the smaller dimension
106        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        // Determine orientation
116        self.orientation = if window_size.x > window_size.y {
117            Orientation::Landscape
118        } else {
119            Orientation::Portrait
120        };
121    }
122
123    /// Get responsive font size
124    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    /// Get responsive spacing
144    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    /// Get responsive border radius
164    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    /// Get responsive margin as i8 (egui compatible)
184    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    /// Get default container margin
204    pub fn container_margin(&self) -> i8 {
205        self.margin(ResponsiveMargin::Medium)
206    }
207
208    /// Get egui::Margin with equal margins on all sides
209    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    /// Get default container margin as egui::Margin
215    pub fn container_margin_egui(&self) -> egui::Margin {
216        self.margin_all(ResponsiveMargin::Medium)
217    }
218
219    /// Get symmetric egui::Margin (horizontal and vertical)
220    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    /// Get custom egui::Margin for each side
231    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    /// Check if device is mobile
247    pub fn is_mobile(&self) -> bool {
248        self.device_type == DeviceType::Mobile
249    }
250
251    /// Check if device is tablet
252    pub fn is_tablet(&self) -> bool {
253        self.device_type == DeviceType::Tablet
254    }
255
256    /// Check if device is desktop
257    pub fn is_desktop(&self) -> bool {
258        self.device_type == DeviceType::Desktop
259    }
260
261    /// Check if orientation is portrait
262    pub fn is_portrait(&self) -> bool {
263        self.orientation == Orientation::Portrait
264    }
265}
266
267/// System to update responsive info when window size changes
268pub 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
278/// Plugin for responsive UI system
279pub 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}