1use egui::{Color32, CornerRadius, Frame, InnerResponse, Margin, Ui};
19
20use crate::theme::ThemeExt;
21
22#[derive(Debug, Clone, Copy, PartialEq, Eq)]
24pub enum PanelPosition {
25 Left,
27 RightWidgetBar,
29 RightContent,
31 Top,
33 Bottom,
35 Chart,
37}
38
39pub struct StyledPanelFrame {
67 position: PanelPosition,
68 gap_color: Color32,
69 panel_bg: Color32,
70 rounding: u8,
71 gap: i8,
72}
73
74impl StyledPanelFrame {
75 pub fn left(ui: &Ui) -> Self {
79 let theme = ui.active_theme();
80 let visuals = ui.visuals();
81 Self {
82 position: PanelPosition::Left,
83 gap_color: visuals.faint_bg_color,
84 panel_bg: visuals.panel_fill,
85 rounding: CornerRadius::from(theme.radius.sm).nw,
86 gap: theme.spacing.sm as i8,
87 }
88 }
89
90 pub fn right_widget_bar(ui: &Ui) -> Self {
94 let theme = ui.active_theme();
95 let visuals = ui.visuals();
96 Self {
97 position: PanelPosition::RightWidgetBar,
98 gap_color: visuals.faint_bg_color,
99 panel_bg: visuals.panel_fill,
100 rounding: CornerRadius::from(theme.radius.sm).nw,
101 gap: theme.spacing.sm as i8,
102 }
103 }
104
105 pub fn right_content(ui: &Ui) -> Self {
109 let theme = ui.active_theme();
110 let visuals = ui.visuals();
111 Self {
112 position: PanelPosition::RightContent,
113 gap_color: visuals.faint_bg_color,
114 panel_bg: visuals.panel_fill,
115 rounding: CornerRadius::from(theme.radius.sm).nw,
116 gap: theme.spacing.sm as i8,
117 }
118 }
119
120 pub fn top(ui: &Ui) -> Self {
124 let theme = ui.active_theme();
125 let visuals = ui.visuals();
126 Self {
127 position: PanelPosition::Top,
128 gap_color: visuals.faint_bg_color,
129 panel_bg: visuals.panel_fill,
130 rounding: CornerRadius::from(theme.radius.sm).nw,
131 gap: theme.spacing.sm as i8,
132 }
133 }
134
135 pub fn bottom(ui: &Ui) -> Self {
139 let theme = ui.active_theme();
140 let visuals = ui.visuals();
141 Self {
142 position: PanelPosition::Bottom,
143 gap_color: visuals.faint_bg_color,
144 panel_bg: visuals.panel_fill,
145 rounding: CornerRadius::from(theme.radius.sm).nw,
146 gap: theme.spacing.sm as i8,
147 }
148 }
149
150 pub fn chart(ui: &Ui, chart_bg: Color32) -> Self {
154 let theme = ui.active_theme();
155 let visuals = ui.visuals();
156 Self {
157 position: PanelPosition::Chart,
158 gap_color: visuals.faint_bg_color,
159 panel_bg: chart_bg,
160 rounding: CornerRadius::from(theme.radius.sm).nw,
161 gap: theme.spacing.sm as i8,
162 }
163 }
164
165 #[must_use]
167 pub fn gap_color(mut self, color: Color32) -> Self {
168 self.gap_color = color;
169 self
170 }
171
172 #[must_use]
174 pub fn panel_bg(mut self, color: Color32) -> Self {
175 self.panel_bg = color;
176 self
177 }
178
179 pub fn outer_frame(&self) -> Frame {
186 let margin = match self.position {
187 PanelPosition::Left => Margin {
189 left: 0,
190 right: self.gap,
191 top: 0, bottom: 0,
193 },
194 PanelPosition::RightWidgetBar => Margin {
196 left: self.gap,
197 right: 0,
198 top: 0, bottom: 0,
200 },
201 PanelPosition::RightContent => Margin {
203 left: self.gap,
204 right: 0,
205 top: 0, bottom: 0,
207 },
208 PanelPosition::Top => Margin {
210 left: 0,
211 right: 0,
212 top: 0,
213 bottom: self.gap,
214 },
215 PanelPosition::Bottom => Margin {
217 left: 0,
218 right: 0,
219 top: self.gap,
220 bottom: 0,
221 },
222 PanelPosition::Chart => Margin::ZERO,
224 };
225
226 Frame::new()
227 .fill(self.gap_color)
228 .corner_radius(CornerRadius::ZERO)
229 .inner_margin(margin)
230 }
231
232 pub fn inner_frame(&self) -> Frame {
234 let rounding = match self.position {
235 PanelPosition::Left => CornerRadius {
236 nw: 0,
237 ne: self.rounding,
238 sw: 0,
239 se: 0,
240 },
241 PanelPosition::RightWidgetBar | PanelPosition::RightContent => CornerRadius {
242 nw: self.rounding,
243 ne: 0,
244 sw: 0,
245 se: 0,
246 },
247 PanelPosition::Top | PanelPosition::Bottom => CornerRadius::ZERO,
248 PanelPosition::Chart => CornerRadius {
249 nw: self.rounding,
250 ne: self.rounding,
251 sw: 0,
252 se: 0,
253 },
254 };
255
256 Frame::new().fill(self.panel_bg).corner_radius(rounding)
257 }
258
259 pub fn show<R>(self, ui: &mut Ui, content: impl FnOnce(&mut Ui) -> R) -> InnerResponse<R> {
263 self.outer_frame()
264 .show(ui, |ui| self.inner_frame().show(ui, content).inner)
265 }
266}
267
268pub struct Card {
283 rounding: Option<f32>,
284 padding: Option<f32>,
285}
286
287impl Default for Card {
288 fn default() -> Self {
289 Self::new()
290 }
291}
292
293impl Card {
294 pub fn new() -> Self {
296 Self {
297 rounding: None,
298 padding: None,
299 }
300 }
301
302 #[must_use]
304 pub fn rounding(mut self, r: f32) -> Self {
305 self.rounding = Some(r);
306 self
307 }
308
309 #[must_use]
311 pub fn padding(mut self, p: f32) -> Self {
312 self.padding = Some(p);
313 self
314 }
315
316 pub fn show<R>(self, ui: &mut Ui, content: impl FnOnce(&mut Ui) -> R) -> InnerResponse<R> {
318 let theme = ui.active_theme();
319 Frame::new()
320 .fill(theme.ui.panel_bg_secondary)
321 .stroke(ui.visuals().widgets.noninteractive.bg_stroke)
322 .corner_radius(self.rounding.unwrap_or(theme.radius.sm))
323 .inner_margin(self.padding.unwrap_or(theme.spacing.lg))
324 .show(ui, content)
325 }
326}
327
328pub fn modern_panel_frame(bg: Color32) -> Frame {
332 Frame::new()
333 .fill(bg)
334 .corner_radius(CornerRadius::ZERO)
335 .inner_margin(egui::Margin::ZERO)
336 .outer_margin(egui::Margin::ZERO)
337 .stroke(egui::Stroke::NONE)
338}
339
340#[cfg(test)]
341mod tests {
342 use super::*;
343
344 #[test]
345 fn separate_panel_frames_use_the_applied_gap_and_radius() {
346 for mut theme in [crate::theme::Theme::light(), crate::theme::Theme::dark()] {
347 theme.spacing.sm = 11.0;
348 theme.radius.sm = 7.5;
349 let ctx = egui::Context::default();
350 crate::theme::apply_to_egui(&ctx, &theme);
351 ctx.run_ui(Default::default(), |ui| {
352 for (frame, gap, corners) in [
353 (StyledPanelFrame::left(ui), [0, 11, 0, 0], [0, 8]),
354 (
355 StyledPanelFrame::right_widget_bar(ui),
356 [11, 0, 0, 0],
357 [8, 0],
358 ),
359 (StyledPanelFrame::right_content(ui), [11, 0, 0, 0], [8, 0]),
360 (StyledPanelFrame::top(ui), [0, 0, 0, 11], [0, 0]),
361 (StyledPanelFrame::bottom(ui), [0, 0, 11, 0], [0, 0]),
362 (
363 StyledPanelFrame::chart(ui, theme.chart.bg),
364 [0, 0, 0, 0],
365 [8, 8],
366 ),
367 ] {
368 let margin = frame.outer_frame().inner_margin;
369 assert_eq!([margin.left, margin.right, margin.top, margin.bottom], gap);
370 let radius = frame.inner_frame().corner_radius;
371 assert_eq!([radius.nw, radius.ne], corners);
372 assert_eq!([radius.sw, radius.se], [0, 0]);
373 }
374 })
375 .drop_without_applying_deltas();
376 }
377 }
378}