armas_basic/components/
card.rs1use crate::ext::ArmasContextExt;
29use egui::{self, Color32, CornerRadius};
30
31const CORNER_RADIUS: f32 = 8.0; const PADDING: f32 = 24.0; const BORDER_WIDTH: f32 = 1.0;
35
36#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
38pub enum CardVariant {
39 Filled,
41 #[default]
43 Outlined,
44 Elevated,
46}
47
48pub struct Card<'a> {
50 pub title: Option<&'a str>,
52 pub variant: CardVariant,
54 pub clickable: bool,
56 pub width: Option<f32>,
58 pub height: Option<f32>,
60 pub min_height: Option<f32>,
62 pub max_height: Option<f32>,
64 pub inner_margin: Option<f32>,
66 pub margin: Option<egui::Margin>,
68 pub fill_color: Option<Color32>,
70 pub stroke_color: Option<Color32>,
72 pub corner_radius: Option<f32>,
74}
75
76impl<'a> Card<'a> {
77 #[must_use]
79 pub const fn new() -> Self {
80 Self {
81 title: None,
82 variant: CardVariant::Filled,
83 clickable: false,
84 width: None,
85 height: None,
86 min_height: None,
87 max_height: None,
88 inner_margin: None,
89 margin: None,
90 fill_color: None,
91 stroke_color: None,
92 corner_radius: None,
93 }
94 }
95
96 #[must_use]
98 pub const fn title(mut self, title: &'a str) -> Self {
99 self.title = Some(title);
100 self
101 }
102
103 #[must_use]
105 pub const fn height(mut self, height: f32) -> Self {
106 self.height = Some(height);
107 self
108 }
109
110 #[must_use]
112 pub const fn min_height(mut self, height: f32) -> Self {
113 self.min_height = Some(height);
114 self
115 }
116
117 #[must_use]
119 pub const fn max_height(mut self, height: f32) -> Self {
120 self.max_height = Some(height);
121 self
122 }
123
124 #[must_use]
126 pub const fn variant(mut self, variant: CardVariant) -> Self {
127 self.variant = variant;
128 self
129 }
130
131 #[must_use]
133 pub const fn clickable(mut self, clickable: bool) -> Self {
134 self.clickable = clickable;
135 self
136 }
137
138 #[must_use]
140 pub const fn width(mut self, width: f32) -> Self {
141 self.width = Some(width);
142 self
143 }
144
145 #[must_use]
147 pub const fn inner_margin(mut self, margin: f32) -> Self {
148 self.inner_margin = Some(margin);
149 self
150 }
151
152 #[must_use]
155 pub const fn margin(mut self, margin: egui::Margin) -> Self {
156 self.margin = Some(margin);
157 self
158 }
159
160 #[must_use]
162 pub const fn fill(mut self, color: Color32) -> Self {
163 self.fill_color = Some(color);
164 self
165 }
166
167 #[must_use]
169 pub const fn stroke(mut self, color: Color32) -> Self {
170 self.stroke_color = Some(color);
171 self
172 }
173
174 #[must_use]
176 pub const fn corner_radius(mut self, radius: f32) -> Self {
177 self.corner_radius = Some(radius);
178 self
179 }
180
181 #[must_use]
183 pub const fn rounding(mut self, radius: f32) -> Self {
184 self.corner_radius = Some(radius);
185 self
186 }
187
188 #[must_use]
190 pub const fn hover_effect(mut self, enable: bool) -> Self {
191 self.clickable = enable;
192 self
193 }
194
195 pub fn show<R>(
201 self,
202 ui: &mut egui::Ui,
203 content: impl FnOnce(&mut egui::Ui) -> R,
204 ) -> CardResponse<R> {
205 let theme = ui.ctx().armas_theme();
206 let (fill_color, border_width, border_color) = match self.variant {
208 CardVariant::Filled => {
209 (
211 self.fill_color.unwrap_or_else(|| theme.muted()),
212 0.0,
213 Color32::TRANSPARENT,
214 )
215 }
216 CardVariant::Outlined => {
217 (
219 self.fill_color.unwrap_or_else(|| theme.card()),
220 BORDER_WIDTH,
221 self.stroke_color.unwrap_or_else(|| theme.border()),
222 )
223 }
224 CardVariant::Elevated => {
225 (
227 self.fill_color.unwrap_or_else(|| theme.card()),
228 BORDER_WIDTH,
229 self.stroke_color
230 .unwrap_or_else(|| theme.border().gamma_multiply(0.5)),
231 )
232 }
233 };
234
235 let corner_rad = self.corner_radius.unwrap_or(CORNER_RADIUS) as u8;
236
237 let sense = if self.clickable {
238 egui::Sense::click()
239 } else {
240 egui::Sense::hover()
241 };
242
243 let frame_margin = self.margin.unwrap_or_else(|| {
245 let margin_val = self.inner_margin.unwrap_or(PADDING) as i8;
246 egui::Margin::same(margin_val)
247 });
248 let mut content_result = None;
249
250 let outer_response = if let (Some(width), Some(height)) = (self.width, self.height) {
252 let desired_size = egui::Vec2::new(width, height);
253 let (rect, _) = ui.allocate_exact_size(desired_size, sense);
254
255 let mut child_ui = ui.new_child(
258 egui::UiBuilder::new()
259 .max_rect(rect)
260 .layout(egui::Layout::top_down(egui::Align::Min)),
261 );
262
263 let frame_response = egui::Frame::new()
264 .fill(fill_color)
265 .corner_radius(CornerRadius::same(corner_rad))
266 .stroke(egui::Stroke::new(border_width, border_color))
267 .inner_margin(frame_margin)
268 .outer_margin(0.0) .show(&mut child_ui, |ui| {
270 if let Some(title) = self.title {
272 ui.label(
273 egui::RichText::new(title)
274 .size(ui.spacing().interact_size.y * 0.7)
275 .color(theme.foreground())
276 .strong(),
277 );
278 ui.add_space(theme.spacing.sm);
279 }
280
281 content_result = Some(content(ui));
283 });
284
285 frame_response
286 } else {
287 ui.vertical(|ui| {
289 if let Some(width) = self.width {
291 ui.set_max_width(width);
292 }
293
294 if let Some(height) = self.height {
296 ui.set_height(height);
297 }
298 if let Some(min_height) = self.min_height {
299 ui.set_min_height(min_height);
300 }
301 if let Some(max_height) = self.max_height {
302 ui.set_max_height(max_height);
303 }
304
305 let frame_response = egui::Frame::new()
306 .fill(fill_color)
307 .corner_radius(CornerRadius::same(corner_rad))
308 .stroke(egui::Stroke::new(border_width, border_color))
309 .inner_margin(frame_margin)
310 .outer_margin(0.0) .show(ui, |ui| {
312 if let Some(title) = self.title {
314 ui.label(
315 egui::RichText::new(title)
316 .size(ui.spacing().interact_size.y * 0.7)
317 .color(theme.foreground())
318 .strong(),
319 );
320 ui.add_space(theme.spacing.sm);
321 }
322
323 content_result = Some(content(ui));
325 });
326
327 frame_response
328 })
329 .inner
330 };
331
332 let rect = outer_response.response.rect;
334 let response = if self.clickable {
335 ui.interact(rect, ui.id().with("card"), sense)
336 } else {
337 outer_response.response
338 };
339
340 if self.clickable && response.hovered() {
342 ui.painter()
343 .rect_filled(rect, CornerRadius::same(corner_rad), theme.accent());
344 }
345
346 CardResponse {
347 response,
348 inner: content_result.expect("content should be set during frame render"),
349 }
350 }
351}
352
353impl Default for Card<'_> {
354 fn default() -> Self {
355 Self::new()
356 }
357}
358
359pub struct CardResponse<R> {
361 pub response: egui::Response,
363 pub inner: R,
365}
366
367impl<R> CardResponse<R> {
368 pub fn clicked(&self) -> bool {
370 self.response.clicked()
371 }
372
373 pub fn hovered(&self) -> bool {
375 self.response.hovered()
376 }
377}