Skip to main content

egui_charts/ui_kit/frames/
mod.rs

1//! Styled Panel Frames
2//!
3//! Builder pattern for creating panel frames with consistent gaps and selective corner rounding.
4//!
5//! Uses a "gap-based" layout where panels are separated by a background gap color,
6//! and inner corners are rounded where panels meet.
7//!
8//! # Example
9//! ```
10//! # use egui_charts::ui_kit::frames::StyledPanelFrame;
11//! # egui::__run_test_ui(|ui| {
12//! StyledPanelFrame::left(ui).show(ui, |ui| {
13//!     // Panel content
14//! });
15//! # });
16//! ```
17
18use egui::{Color32, CornerRadius, Frame, InnerResponse, Margin, Ui};
19
20use crate::theme::ThemeExt;
21
22/// Panel position in the layout
23#[derive(Debug, Clone, Copy, PartialEq, Eq)]
24pub enum PanelPosition {
25    /// Left sidebar (drawing toolbar)
26    Left,
27    /// Right widget bar (icons)
28    RightWidgetBar,
29    /// Right content panel (alerts, object tree, etc.)
30    RightContent,
31    /// Top panel (toolbar, symbol header)
32    Top,
33    /// Bottom panel (timeline, status)
34    Bottom,
35    /// Central chart area
36    Chart,
37}
38
39/// Styled panel frame builder
40///
41/// Creates consistent outer (gap) and inner (content) frames for panels.
42/// Handles selective corner rounding patterns where panels meet.
43///
44/// # Example
45/// ```
46/// # use egui_charts::ui_kit::frames::StyledPanelFrame;
47/// # egui::__run_test_ui(|ui| {
48/// // Method 1: Show both frames with content
49/// egui::CentralPanel::default().show(ui, |ui| {
50///     StyledPanelFrame::left(ui).show(ui, |ui| {
51///         ui.label("Panel content");
52///     });
53/// });
54///
55/// // Method 2: Get frames separately (for a side panel)
56/// let frame = StyledPanelFrame::left(ui);
57/// egui::Panel::left("left")
58///     .frame(frame.outer_frame())
59///     .show(ui, |ui| {
60///         frame.inner_frame().show(ui, |ui| {
61///             ui.label("Panel content");
62///         });
63///     });
64/// # });
65/// ```
66pub struct StyledPanelFrame {
67    position: PanelPosition,
68    gap_color: Color32,
69    panel_bg: Color32,
70    rounding: u8,
71    gap: i8,
72}
73
74impl StyledPanelFrame {
75    /// Create a left panel frame (drawing toolbar)
76    ///
77    /// Rounded: top-right corner only
78    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    /// Create a right widget bar frame (icon tabs)
91    ///
92    /// Rounded: top-left corner only
93    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    /// Create a right content panel frame (alerts, object tree, etc.)
106    ///
107    /// Rounded: top-left corner only
108    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    /// Create a top panel frame (toolbar)
121    ///
122    /// No rounding - spans full width
123    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    /// Create a bottom panel frame (timeline)
136    ///
137    /// No rounding
138    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    /// Create a central chart area frame
151    ///
152    /// Rounded: top-left and top-right corners
153    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    /// Override the gap color
166    #[must_use]
167    pub fn gap_color(mut self, color: Color32) -> Self {
168        self.gap_color = color;
169        self
170    }
171
172    /// Override the panel background color
173    #[must_use]
174    pub fn panel_bg(mut self, color: Color32) -> Self {
175        self.panel_bg = color;
176        self
177    }
178
179    /// Get the outer frame (gap color with margins)
180    ///
181    /// Panel layout: gaps are created by panel margins
182    /// - Top toolbar: bottom margin (gap below toolbar)
183    /// - Side panels: horizontal margins only (NO top margin)
184    /// - Chart: no margins (gaps from surrounding panels)
185    pub fn outer_frame(&self) -> Frame {
186        let margin = match self.position {
187            // Left sidebar: right margin only (gap to chart)
188            PanelPosition::Left => Margin {
189                left: 0,
190                right: self.gap,
191                top: 0, // NO top margin - toolbar provides gap via its bottom margin
192                bottom: 0,
193            },
194            // Right widget bar: left margin only (gap to chart)
195            PanelPosition::RightWidgetBar => Margin {
196                left: self.gap,
197                right: 0,
198                top: 0, // NO top margin
199                bottom: 0,
200            },
201            // Right content panel: left margin only
202            PanelPosition::RightContent => Margin {
203                left: self.gap,
204                right: 0,
205                top: 0, // NO top margin
206                bottom: 0,
207            },
208            // Top panel: bottom margin for gap below
209            PanelPosition::Top => Margin {
210                left: 0,
211                right: 0,
212                top: 0,
213                bottom: self.gap,
214            },
215            // Bottom panel: top margin for gap above
216            PanelPosition::Bottom => Margin {
217                left: 0,
218                right: 0,
219                top: self.gap,
220                bottom: 0,
221            },
222            // Chart: no margins (gaps provided by surrounding panels)
223            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    /// Get the inner frame (panel bg with selective rounding)
233    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    /// Show both outer and inner frames with content
260    ///
261    /// This is a convenience method that nests the frames correctly.
262    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
268// =============================================================================
269// Card: a bordered surface one step above its parent
270// =============================================================================
271
272/// Card container: the theme's secondary surface with a 1 px border, rounding and padding.
273///
274/// ```
275/// # use egui_charts::ui_kit::Card;
276/// # egui::__run_test_ui(|ui| {
277/// Card::new().show(ui, |ui| {
278///     ui.label("Content in a card");
279/// });
280/// # });
281/// ```
282pub 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    /// Create a card with the theme's small radius and large padding
295    pub fn new() -> Self {
296        Self {
297            rounding: None,
298            padding: None,
299        }
300    }
301
302    /// Override the corner rounding
303    #[must_use]
304    pub fn rounding(mut self, r: f32) -> Self {
305        self.rounding = Some(r);
306        self
307    }
308
309    /// Override the inner padding
310    #[must_use]
311    pub fn padding(mut self, p: f32) -> Self {
312        self.padding = Some(p);
313        self
314    }
315
316    /// Show the card with content
317    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
328/// Create a simple modern frame (no gaps, no rounding)
329///
330/// Used for the Modern layout style where panels are edge-to-edge with separator lines.
331pub 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}