Skip to main content

egui_charts/ui_kit/section/
mod.rs

1//! Section Component
2//!
3//! A collapsible section with header and animated expand/collapse.
4//!
5//! # Usage
6//!
7//! ```
8//! use egui_charts::ui_kit::section::Section;
9//!
10//! # egui::__run_test_ui(|ui| {
11//! Section::new("settings_section", "Settings")
12//!     .default_open(true)
13//!     .show(ui, |ui| {
14//!         ui.label("Section content here");
15//!     });
16//! # });
17//! ```
18
19use crate::localization as l10n;
20use egui::text::LayoutJob;
21use egui::{Align, CollapsingResponse, Id, RichText, TextStyle, Ui, Vec2, WidgetText};
22
23use crate::icons::{Icon, icons};
24use crate::theme::ThemeExt;
25use crate::tokens::DESIGN_TOKENS;
26use egui::emath::GuiRounding;
27
28/// A collapsible section with header
29pub struct Section {
30    id: Id,
31    title: String,
32    default_open: bool,
33    icon: Option<&'static Icon>,
34    strong_header: bool,
35}
36
37impl Section {
38    /// Create a new section
39    pub fn new(id: impl std::hash::Hash + std::fmt::Debug, title: impl Into<String>) -> Self {
40        Self {
41            id: Id::unique(id),
42            title: title.into(),
43            default_open: true,
44            icon: None,
45            strong_header: true,
46        }
47    }
48
49    /// Set whether the section is open by default
50    pub fn default_open(mut self, open: bool) -> Self {
51        self.default_open = open;
52        self
53    }
54
55    /// Set an icon for the section header
56    pub fn icon(mut self, icon: &'static Icon) -> Self {
57        self.icon = Some(icon);
58        self
59    }
60
61    /// Set whether the header text should be bold
62    pub fn strong_header(mut self, strong: bool) -> Self {
63        self.strong_header = strong;
64        self
65    }
66
67    /// Show the section
68    pub fn show<R>(
69        self,
70        ui: &mut Ui,
71        add_contents: impl FnOnce(&mut Ui) -> R,
72    ) -> CollapsingResponse<R> {
73        let theme = ui.active_theme();
74        let mut text = RichText::new(l10n::text(ui.ctx(), &self.title)).size(theme.text.small);
75        if self.strong_header {
76            text = text.strong();
77        }
78        let icon_size = DESIGN_TOKENS.sizing.icon_sm;
79
80        let header_text: WidgetText = if self.icon.is_some() {
81            // Reserve the icon and its gap before the title, inside the header's own text.
82            let mut job = LayoutJob::default();
83            text.append_to(
84                &mut job,
85                ui.style(),
86                TextStyle::Button.into(),
87                Align::Center,
88            );
89            job.sections[0].leading_space = icon_size + theme.spacing.lg;
90            job.into()
91        } else {
92            text.into()
93        };
94
95        let header = egui::CollapsingHeader::new(header_text)
96            .id_salt(self.id)
97            .default_open(self.default_open);
98
99        let header = header.icon(move |ui, openness, response| {
100            let tint = ui.visuals().widgets.noninteractive.fg_stroke.color;
101            let chevron = if openness > 0.5 {
102                &icons::CHEVRON_DOWN
103            } else {
104                &icons::CHEVRON_RIGHT
105            };
106            let rect = egui::Rect::from_center_size(response.rect.center(), Vec2::splat(icon_size))
107                .round_to_pixels(ui.pixels_per_point());
108            chevron
109                .as_image_tinted(rect.size(), tint)
110                .paint_at(ui, rect);
111            if let Some(icon) = self.icon {
112                // The title reserves this icon slot after the disclosure control.
113                let left = response.rect.center().x + ui.spacing().indent / 2.0;
114                let rect = egui::Rect::from_min_size(
115                    egui::pos2(left, response.rect.center().y - icon_size / 2.0),
116                    Vec2::splat(icon_size),
117                )
118                .round_to_pixels(ui.pixels_per_point());
119                icon.as_image_tinted(rect.size(), tint).paint_at(ui, rect);
120            }
121        });
122
123        header.show(ui, add_contents)
124    }
125}
126
127/// Extension trait for `egui::Ui` to easily create sections
128pub trait SectionExt {
129    /// Create a simple section with just a title
130    fn section<R>(
131        &mut self,
132        title: impl Into<String>,
133        add_contents: impl FnOnce(&mut Ui) -> R,
134    ) -> CollapsingResponse<R>;
135
136    /// Create a section that's closed by default
137    fn collapsed_section<R>(
138        &mut self,
139        title: impl Into<String>,
140        add_contents: impl FnOnce(&mut Ui) -> R,
141    ) -> CollapsingResponse<R>;
142}
143
144impl SectionExt for Ui {
145    fn section<R>(
146        &mut self,
147        title: impl Into<String>,
148        add_contents: impl FnOnce(&mut Ui) -> R,
149    ) -> CollapsingResponse<R> {
150        let title = title.into();
151        Section::new(&title, &title)
152            .default_open(true)
153            .show(self, add_contents)
154    }
155
156    fn collapsed_section<R>(
157        &mut self,
158        title: impl Into<String>,
159        add_contents: impl FnOnce(&mut Ui) -> R,
160    ) -> CollapsingResponse<R> {
161        let title = title.into();
162        Section::new(&title, &title)
163            .default_open(false)
164            .show(self, add_contents)
165    }
166}