konnektoren_bevy/ui/
widgets.rs

1use super::responsive::{ResponsiveFontSize, ResponsiveInfo};
2use crate::theme::KonnektorenTheme;
3use bevy::prelude::*;
4use bevy_egui::egui;
5
6/// A reusable button widget styled with the Konnektoren theme.
7pub struct ThemedButton<'a> {
8    pub label: &'a str,
9    pub theme: &'a KonnektorenTheme,
10    pub enabled: bool,
11    pub width: Option<f32>,
12    pub opacity: f32,
13    pub responsive_info: Option<&'a ResponsiveInfo>,
14    pub custom_style: Option<Box<dyn FnOnce(egui::Button<'a>) -> egui::Button<'a> + 'a>>,
15}
16
17impl<'a> ThemedButton<'a> {
18    pub fn new(label: &'a str, theme: &'a KonnektorenTheme) -> Self {
19        Self {
20            label,
21            theme,
22            enabled: true,
23            width: None,
24            opacity: 1.0,
25            responsive_info: None,
26            custom_style: None,
27        }
28    }
29
30    pub fn responsive(mut self, responsive_info: &'a ResponsiveInfo) -> Self {
31        self.responsive_info = Some(responsive_info);
32        self
33    }
34
35    pub fn opacity(mut self, opacity: f32) -> Self {
36        self.opacity = opacity;
37        self
38    }
39
40    pub fn width(mut self, width: f32) -> Self {
41        self.width = Some(width);
42        self
43    }
44
45    pub fn enabled(mut self, enabled: bool) -> Self {
46        self.enabled = enabled;
47        self
48    }
49
50    /// Apply custom styling to the button
51    pub fn with_style<F>(mut self, style_fn: F) -> Self
52    where
53        F: FnOnce(egui::Button<'a>) -> egui::Button<'a> + 'a,
54    {
55        self.custom_style = Some(Box::new(style_fn));
56        self
57    }
58}
59
60impl<'a> egui::Widget for ThemedButton<'a> {
61    fn ui(self, ui: &mut egui::Ui) -> egui::Response {
62        self.show(ui)
63    }
64}
65
66impl<'a> ThemedButton<'a> {
67    /// Show the button
68    pub fn show(self, ui: &mut egui::Ui) -> egui::Response {
69        // Determine font size and minimum size based on responsiveness
70        let (font_size, min_height, min_width) = if let Some(responsive_info) = self.responsive_info
71        {
72            let font_size = responsive_info.font_size(ResponsiveFontSize::Medium);
73            let min_height = if responsive_info.is_mobile() {
74                44.0
75            } else {
76                32.0
77            };
78            let min_width = if responsive_info.is_mobile() {
79                120.0
80            } else {
81                80.0
82            };
83            (font_size, min_height, min_width)
84        } else {
85            (18.0, 32.0, 80.0)
86        };
87
88        let mut button = egui::Button::new(
89            egui::RichText::new(self.label)
90                .color(self.theme.primary_content.linear_multiply(self.opacity))
91                .size(font_size),
92        )
93        .fill(self.theme.primary.linear_multiply(self.opacity));
94
95        // Apply custom styling if provided
96        if let Some(style_fn) = self.custom_style {
97            button = style_fn(button);
98        }
99
100        // Set minimum size for touch targets on mobile
101        let final_width = self.width.unwrap_or(min_width).max(min_width);
102        button = button.min_size(egui::vec2(final_width, min_height));
103
104        ui.add_enabled(self.enabled, button)
105    }
106
107    /// Get the configured font size for external use
108    pub fn get_font_size(&self) -> f32 {
109        if let Some(responsive_info) = self.responsive_info {
110            responsive_info.font_size(ResponsiveFontSize::Medium)
111        } else {
112            18.0
113        }
114    }
115
116    /// Get the configured minimum dimensions for external use
117    pub fn get_min_dimensions(&self) -> (f32, f32) {
118        if let Some(responsive_info) = self.responsive_info {
119            let min_height = if responsive_info.is_mobile() {
120                44.0
121            } else {
122                32.0
123            };
124            let min_width = if responsive_info.is_mobile() {
125                120.0
126            } else {
127                80.0
128            };
129            (min_width, min_height)
130        } else {
131            (80.0, 32.0)
132        }
133    }
134}
135
136/// Responsive text widget that adjusts size based on device type
137pub struct ResponsiveText<'a> {
138    pub text: &'a str,
139    pub font_size_type: ResponsiveFontSize,
140    pub color: egui::Color32,
141    pub responsive_info: Option<&'a ResponsiveInfo>,
142    pub strong: bool,
143}
144
145impl<'a> ResponsiveText<'a> {
146    pub fn new(text: &'a str, font_size_type: ResponsiveFontSize, color: egui::Color32) -> Self {
147        Self {
148            text,
149            font_size_type,
150            color,
151            responsive_info: None,
152            strong: false,
153        }
154    }
155
156    pub fn responsive(mut self, responsive_info: &'a ResponsiveInfo) -> Self {
157        self.responsive_info = Some(responsive_info);
158        self
159    }
160
161    pub fn strong(mut self) -> Self {
162        self.strong = true;
163        self
164    }
165}
166
167impl<'a> egui::Widget for ResponsiveText<'a> {
168    fn ui(self, ui: &mut egui::Ui) -> egui::Response {
169        let font_size = if let Some(responsive_info) = self.responsive_info {
170            responsive_info.font_size(self.font_size_type)
171        } else {
172            match self.font_size_type {
173                ResponsiveFontSize::Small => 14.0,
174                ResponsiveFontSize::Medium => 18.0,
175                ResponsiveFontSize::Large => 24.0,
176                ResponsiveFontSize::Header => 32.0,
177                ResponsiveFontSize::Title => 28.0,
178            }
179        };
180
181        let mut rich_text = egui::RichText::new(self.text)
182            .size(font_size)
183            .color(self.color);
184
185        if self.strong {
186            rich_text = rich_text.strong();
187        }
188
189        ui.label(rich_text)
190    }
191}
192
193/// A reusable egui widget for displaying a loading spinner.
194pub struct SpinnerWidget<'a> {
195    pub theme: &'a KonnektorenTheme,
196    pub size: f32,
197    pub responsive_info: Option<&'a ResponsiveInfo>,
198}
199
200impl<'a> SpinnerWidget<'a> {
201    pub fn new(theme: &'a KonnektorenTheme, size: f32) -> Self {
202        Self {
203            theme,
204            size,
205            responsive_info: None,
206        }
207    }
208
209    pub fn responsive(mut self, responsive_info: &'a ResponsiveInfo) -> Self {
210        self.responsive_info = Some(responsive_info);
211        self
212    }
213}
214
215impl<'a> egui::Widget for SpinnerWidget<'a> {
216    fn ui(self, ui: &mut egui::Ui) -> egui::Response {
217        // Adjust spinner size based on device type
218        let final_size = if let Some(responsive_info) = self.responsive_info {
219            let scale_factor = match responsive_info.device_type {
220                crate::ui::responsive::DeviceType::Mobile => 1.2,
221                crate::ui::responsive::DeviceType::Tablet => 1.1,
222                crate::ui::responsive::DeviceType::Desktop => 1.0,
223            };
224            self.size * scale_factor
225        } else {
226            self.size
227        };
228
229        let (rect, response) =
230            ui.allocate_exact_size(egui::vec2(final_size, final_size), egui::Sense::hover());
231        let center = rect.center();
232
233        let time = ui.input(|i| i.time);
234        let angle = time % 2.0 * std::f64::consts::PI;
235        let points = 8;
236        let radius = final_size * 0.375;
237
238        let painter = ui.painter();
239
240        for i in 0..points {
241            let phase = angle + i as f64 * 2.0 * std::f64::consts::PI / points as f64;
242            let point_distance = radius * 0.7;
243            let pos = egui::pos2(
244                center.x + (point_distance * phase.cos() as f32),
245                center.y + (point_distance * phase.sin() as f32),
246            );
247
248            let alpha = ((1.0 - (i as f64 / points as f64)) * 0.8 + 0.2) as f32;
249            let color = self.theme.primary.linear_multiply(alpha);
250
251            painter.circle_filled(pos, final_size * 0.075, color);
252        }
253
254        response
255    }
256}