konnektoren_bevy/ui/
widgets.rs1use super::responsive::{ResponsiveFontSize, ResponsiveInfo};
2use crate::theme::KonnektorenTheme;
3use bevy::prelude::*;
4use bevy_egui::egui;
5
6pub 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 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 pub fn show(self, ui: &mut egui::Ui) -> egui::Response {
69 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 if let Some(style_fn) = self.custom_style {
97 button = style_fn(button);
98 }
99
100 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 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 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
136pub 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
193pub 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 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}