egui_charts/ui_kit/section/
mod.rs1use 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
28pub 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 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 pub fn default_open(mut self, open: bool) -> Self {
51 self.default_open = open;
52 self
53 }
54
55 pub fn icon(mut self, icon: &'static Icon) -> Self {
57 self.icon = Some(icon);
58 self
59 }
60
61 pub fn strong_header(mut self, strong: bool) -> Self {
63 self.strong_header = strong;
64 self
65 }
66
67 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 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 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
127pub trait SectionExt {
129 fn section<R>(
131 &mut self,
132 title: impl Into<String>,
133 add_contents: impl FnOnce(&mut Ui) -> R,
134 ) -> CollapsingResponse<R>;
135
136 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}