1use crate::controls::MacInsets;
8use crate::layout::subpixel_snap_rect;
9use crate::theme::MacTheme;
10use egui::{
11 CornerRadius, FontId, InnerResponse, Pos2, Rect, Response, Sense, Stroke, StrokeKind, Ui,
12 UiBuilder, Vec2, WidgetText,
13};
14
15pub struct MacBox<'a> {
36 title: Option<WidgetText>,
37 theme: Option<&'a MacTheme>,
38 fixed_rect: Option<Rect>,
39}
40
41impl<'a> MacBox<'a> {
42 pub fn new(title: impl Into<WidgetText>) -> Self {
44 Self {
45 title: Some(title.into()),
46 theme: None,
47 fixed_rect: None,
48 }
49 }
50
51 pub fn untitled() -> Self {
53 Self {
54 title: None,
55 theme: None,
56 fixed_rect: None,
57 }
58 }
59
60 pub fn theme(mut self, theme: &'a MacTheme) -> Self {
62 self.theme = Some(theme);
63 self
64 }
65
66 pub fn fixed_rect(mut self, rect: Rect) -> Self {
68 self.fixed_rect = Some(rect);
69 self
70 }
71
72 pub fn content_insets(&self) -> MacInsets {
74 let theme_cow = MacTheme::resolve_or_default(self.theme);
75 let m = theme_cow.box_view();
76 MacInsets {
77 top: m.inset_top.points(),
78 bottom: m.inset_bottom.points(),
79 left: m.inset_left.points(),
80 right: m.inset_right.points(),
81 }
82 }
83
84 pub fn corner_radius(&self) -> f32 {
86 let theme_cow = MacTheme::resolve_or_default(self.theme);
87 theme_cow.box_view().corner_radius.points()
88 }
89
90 pub fn border_width(&self) -> f32 {
92 let theme_cow = MacTheme::resolve_or_default(self.theme);
93 theme_cow.box_view().border_width.points()
94 }
95
96 pub fn paint_at(&self, ui: &mut Ui, rect: Rect) {
98 let theme_cow = MacTheme::resolve(self.theme, ui.ctx());
99 let theme: &MacTheme = &theme_cow;
100 let insets = self.content_insets();
101 let corner_r = self.corner_radius();
102 let border_w = self.border_width();
103 let m = theme.box_view();
104 let snapped = subpixel_snap_rect(rect, ui.ctx().pixels_per_point());
105 let box_bg = theme.box_background();
106 theme.register_surface_rect(ui.ctx(), rect.union(snapped), box_bg);
107
108 ui.painter().add(egui::epaint::RectShape::new(
109 snapped,
110 CornerRadius::same(corner_r as u8),
111 box_bg,
112 Stroke::new(border_w, theme.box_border()),
113 StrokeKind::Inside,
114 ));
115
116 if let Some(ref title) = self.title {
117 let font_id = FontId::proportional(m.title_font_size.points());
118 let title_fg = theme.label_on(theme.box_background());
119 let galley = theme.layout_tracked_galley(
120 ui,
121 title.text(),
122 font_id,
123 title_fg,
124 snapped.width(),
125 );
126 let title_pos = Pos2::new(
127 snapped.min.x + insets.left,
128 snapped.min.y + insets.top * 0.5 - galley.size().y * 0.5,
129 );
130 ui.painter().galley(title_pos, galley, title_fg);
131 }
132 }
133
134 pub fn show<R>(
136 self,
137 ui: &mut Ui,
138 add_contents: impl FnOnce(&mut Ui) -> R,
139 ) -> InnerResponse<R> {
140 let theme_cow = MacTheme::resolve(self.theme, ui.ctx());
141 let theme: &MacTheme = &theme_cow;
142 let insets = self.content_insets();
143 let corner_r = self.corner_radius();
144 let border_w = self.border_width();
145 let title_font_sz = theme.box_view().title_font_size.points();
146
147 let (start_pos, available_w, fixed_box_rect) = if let Some(fixed) = self.fixed_rect {
148 (fixed.min, fixed.width(), Some(fixed))
149 } else {
150 (ui.cursor().min, ui.available_width(), None)
151 };
152
153 let content_max_y = fixed_box_rect.map_or(f32::INFINITY, |r| r.max.y - insets.bottom);
154 let content_rect = Rect::from_min_max(
155 Pos2::new(start_pos.x + insets.left, start_pos.y + insets.top),
156 Pos2::new(start_pos.x + available_w - insets.right, content_max_y),
157 );
158
159 let bg_shape_idx = ui.painter().add(egui::Shape::Noop);
160 let box_scope_id = ui.next_auto_id().with("mac_box_content");
161
162 let mut child_ui = ui.new_child(
163 UiBuilder::new()
164 .id_salt(box_scope_id)
165 .max_rect(content_rect),
166 );
167 let box_bg = theme.box_background();
168 child_ui.visuals_mut().panel_fill = box_bg;
169 let surface_slot = theme.register_surface_rect_slot(ui.ctx(), Rect::NOTHING, box_bg);
170 let inner = add_contents(&mut child_ui);
171 let content_min_rect = child_ui.min_rect();
172
173 let (rect, response) = if let Some(fixed) = fixed_box_rect {
174 let r = subpixel_snap_rect(fixed, ui.ctx().pixels_per_point());
175 let resp = ui.interact(r, box_scope_id.with("fixed_box"), Sense::hover());
176 (r, resp)
177 } else {
178 let box_h = insets.top + content_min_rect.height() + insets.bottom;
179 let box_rect = Rect::from_min_size(start_pos, Vec2::new(available_w, box_h));
180 let box_rect = subpixel_snap_rect(box_rect, ui.ctx().pixels_per_point());
181 ui.allocate_exact_size(box_rect.size(), Sense::hover())
182 };
183 theme.update_surface_rect_slot(
184 ui.ctx(),
185 surface_slot,
186 fixed_box_rect.map_or(rect, |f| f.union(rect)),
187 );
188
189 ui.painter().set(
190 bg_shape_idx,
191 egui::epaint::RectShape::new(
192 rect,
193 CornerRadius::same(corner_r as u8),
194 box_bg,
195 Stroke::new(border_w, theme.box_border()),
196 StrokeKind::Inside,
197 ),
198 );
199
200 if let Some(title) = self.title {
201 let title_fg = theme.label_on(box_bg);
202 let galley = theme.layout_tracked_galley(
203 ui,
204 title.text(),
205 FontId::proportional(title_font_sz),
206 title_fg,
207 available_w,
208 );
209 let title_pos = Pos2::new(
210 rect.min.x + insets.left,
211 rect.min.y + insets.top * 0.5 - galley.size().y * 0.5,
212 );
213 ui.painter().galley(title_pos, galley, title_fg);
214 }
215
216 InnerResponse::new(inner, response)
217 }
218
219 pub fn separator(ui: &mut Ui) -> Response {
221 let theme = MacTheme::resolve(None, ui.ctx());
222 let frame_h = theme.box_view().separator_frame_height.points();
223 let border_w = theme.box_view().border_width.points();
224
225 let (rect, response) =
226 ui.allocate_exact_size(Vec2::new(ui.available_width(), frame_h), Sense::hover());
227 let painter = ui.painter_at(rect);
228 let cy = rect.center().y;
229
230 painter.line_segment(
231 [Pos2::new(rect.min.x, cy), Pos2::new(rect.max.x, cy)],
232 Stroke::new(border_w, theme.separator()),
233 );
234
235 response
236 }
237
238 pub fn separator_at(ui: &mut Ui, rect: Rect, theme: &MacTheme) {
240 let border_w = theme.box_view().border_width.points();
241 let stroke = Stroke::new(border_w, theme.separator());
242 let painter = ui.painter();
243 if rect.width() >= rect.height() {
244 let cy = rect.center().y;
245 painter.line_segment([Pos2::new(rect.min.x, cy), Pos2::new(rect.max.x, cy)], stroke);
246 } else {
247 let cx = rect.center().x;
248 painter.line_segment([Pos2::new(cx, rect.min.y), Pos2::new(cx, rect.max.y)], stroke);
249 }
250 }
251}