1use iced_core::{Length, Pixels, Rectangle};
4
5pub struct Style {
7 pub color: iced_core::Background,
9 pub radius: iced_core::border::Radius,
11}
12
13pub trait Catalog {
15 type Class<'a>;
17 fn default<'a>() -> Self::Class<'a>;
19 fn style(&self, class: &Self::Class<'_>) -> Style;
21}
22
23pub type StyleFn<'a, Theme> = Box<dyn Fn(&Theme) -> Style + 'a>;
25
26impl Catalog for iced_widget::Theme {
27 type Class<'a> = StyleFn<'a, Self>;
28
29 fn default<'a>() -> Self::Class<'a> {
30 Box::new(|theme| Style {
31 color: iced_core::Background::Color(theme.extended_palette().secondary.weak.color),
32 radius: iced_core::border::Radius::default(),
33 })
34 }
35
36 fn style(&self, class: &Self::Class<'_>) -> Style {
37 (class)(self)
38 }
39}
40
41pub struct LabeledFrame<'a, Message, Theme, Renderer>
43where
44 Theme: Catalog,
45{
46 title: iced_core::Element<'a, Message, Theme, Renderer>,
47 content: iced_core::Element<'a, Message, Theme, Renderer>,
48 width: Length,
49 height: Length,
50 class: Theme::Class<'a>,
51 inset: f32,
52 outset: f32,
53 stroke_width: f32,
54 horizontal_title_padding: f32,
55}
56
57impl<'a, Message, Theme, Renderer> LabeledFrame<'a, Message, Theme, Renderer>
58where
59 Theme: Catalog,
60{
61 pub fn new(
63 title: impl Into<iced_core::Element<'a, Message, Theme, Renderer>>,
64 content: impl Into<iced_core::Element<'a, Message, Theme, Renderer>>,
65 ) -> Self {
66 Self {
67 title: title.into(),
68 content: content.into(),
69 width: Length::Shrink,
70 height: Length::Shrink,
71 class: Theme::default(),
72 inset: 5.0,
73 outset: 5.0,
74 stroke_width: 3.0,
75 horizontal_title_padding: 5.0,
76 }
77 }
78
79 #[must_use]
81 pub fn width(mut self, width: impl Into<Length>) -> Self {
82 self.width = width.into();
83 self
84 }
85
86 #[must_use]
88 pub fn height(mut self, height: impl Into<Length>) -> Self {
89 self.height = height.into();
90 self
91 }
92
93 #[must_use]
95 pub fn inset(mut self, inset: impl Into<Pixels>) -> Self {
96 self.inset = inset.into().0;
97 self
98 }
99
100 #[must_use]
102 pub fn outset(mut self, outset: impl Into<Pixels>) -> Self {
103 self.outset = outset.into().0;
104 self
105 }
106
107 #[must_use]
109 pub fn stroke_width(mut self, stroke_width: impl Into<Pixels>) -> Self {
110 self.stroke_width = stroke_width.into().0;
111 self
112 }
113
114 #[must_use]
116 pub fn horizontal_title_padding(mut self, padding: impl Into<Pixels>) -> Self {
117 self.horizontal_title_padding = padding.into().0;
118 self
119 }
120
121 #[must_use]
123 pub fn style(mut self, style: impl Fn(&Theme) -> Style + 'a) -> Self
124 where
125 Theme::Class<'a>: From<StyleFn<'a, Theme>>,
126 {
127 self.class = (Box::new(style) as StyleFn<'a, Theme>).into();
128 self
129 }
130}
131
132impl<Message, Theme, Renderer> iced_core::Widget<Message, Theme, Renderer>
133 for LabeledFrame<'_, Message, Theme, Renderer>
134where
135 Renderer: iced_core::Renderer,
136 Theme: Catalog,
137{
138 fn size(&self) -> iced_core::Size<Length> {
139 iced_core::Size::new(self.width, self.height)
140 }
141
142 fn layout(
143 &mut self,
144 tree: &mut iced_core::widget::Tree,
145 renderer: &Renderer,
146 limits: &iced_core::layout::Limits,
147 ) -> iced_core::layout::Node {
148 let limits = (*limits).height(self.height).width(self.width).loose();
149 let title_layout = self
150 .title
151 .as_widget_mut()
152 .layout(
153 &mut tree.children[0],
154 renderer,
155 &limits.shrink(
156 iced_core::Padding::default()
157 .left(
158 (self.outset + self.inset + self.stroke_width) * 2.0
159 + self.horizontal_title_padding,
160 )
161 .right(
162 (self.outset + self.inset + self.stroke_width) * 2.0
163 + self.horizontal_title_padding,
164 ),
165 ),
166 )
167 .translate([
168 (self.outset + self.inset + self.stroke_width) * 2.0
169 + self.horizontal_title_padding,
170 0.0,
171 ]);
172 let content_layout = self
173 .content
174 .as_widget_mut()
175 .layout(
176 &mut tree.children[1],
177 renderer,
178 &limits.shrink(
179 iced_core::Padding::default()
180 .left(self.outset + self.inset + self.stroke_width)
181 .right(self.outset + self.inset + self.stroke_width)
182 .bottom(self.outset + self.inset + self.stroke_width)
183 .top(
184 (self.outset + self.inset + self.stroke_width)
185 .max(title_layout.size().height),
186 ),
187 ),
188 )
189 .translate([
190 self.outset + self.inset + self.stroke_width,
191 (self.outset + self.inset + self.stroke_width).max(title_layout.bounds().height),
192 ]);
193
194 iced_core::layout::Node::with_children(
195 limits.resolve(
196 self.width,
197 self.height,
198 iced_core::Size::new(
199 (content_layout.size().width
200 + (self.outset + self.inset + self.stroke_width) * 2.0)
201 .max(
202 title_layout.size().width
203 + (self.outset + self.inset + self.stroke_width) * 4.0,
204 ),
205 content_layout.bounds().y
206 + content_layout.size().height
207 + (self.outset + self.inset + self.stroke_width),
208 ),
209 ),
210 vec![title_layout, content_layout],
211 )
212 }
213
214 fn children(&self) -> Vec<iced_core::widget::Tree> {
215 vec![
216 iced_core::widget::Tree::new(&self.title),
217 iced_core::widget::Tree::new(&self.content),
218 ]
219 }
220
221 fn diff(&self, tree: &mut iced_core::widget::Tree) {
222 tree.diff_children(&[&self.title, &self.content]);
223 }
224
225 fn draw(
226 &self,
227 tree: &iced_core::widget::Tree,
228 renderer: &mut Renderer,
229 theme: &Theme,
230 style: &iced_core::renderer::Style,
231 layout: iced_core::Layout<'_>,
232 cursor: iced_core::mouse::Cursor,
233 viewport: &iced_core::Rectangle,
234 ) {
235 [&self.title, &self.content]
236 .iter()
237 .zip(&tree.children)
238 .zip(layout.children())
239 .for_each(|((child, state), layout)| {
240 child
241 .as_widget()
242 .draw(state, renderer, theme, style, layout, cursor, viewport);
243 });
244 let style = theme.style(&self.class);
245 let title_layout = layout.children().next().expect("missing title layout");
246 let top_line_y =
247 title_layout.position().y + (title_layout.bounds().height - self.stroke_width) / 2.0;
248 renderer.fill_quad(
250 iced_core::renderer::Quad {
251 bounds: iced_core::Rectangle {
252 x: layout.position().x + self.outset,
253 y: top_line_y,
254 width: self.stroke_width,
255 height: layout.bounds().height
256 - self.outset
257 - (top_line_y - layout.position().y),
258 },
259 border: iced_core::Border {
260 radius: iced_core::border::Radius::default()
261 .top_left(style.radius.top_left)
262 .bottom_left(style.radius.bottom_left),
263 ..Default::default()
264 },
265 shadow: iced_core::Shadow::default(),
266 ..Default::default()
267 },
268 style.color,
269 );
270 renderer.fill_quad(
272 iced_core::renderer::Quad {
273 bounds: iced_core::Rectangle {
274 x: layout.position().x + layout.bounds().width
275 - self.outset
276 - self.stroke_width,
277 y: top_line_y,
278 width: self.stroke_width,
279 height: layout.bounds().height
280 - self.outset
281 - (top_line_y - layout.position().y),
282 },
283 border: iced_core::Border {
284 radius: iced_core::border::Radius::default()
285 .top_right(style.radius.top_right)
286 .bottom_right(style.radius.bottom_right),
287 ..Default::default()
288 },
289 shadow: iced_core::Shadow::default(),
290 ..Default::default()
291 },
292 style.color,
293 );
294 renderer.fill_quad(
296 iced_core::renderer::Quad {
297 bounds: iced_core::Rectangle {
298 x: layout.position().x + self.outset,
299 y: layout.position().y + layout.bounds().height
300 - self.outset
301 - self.stroke_width,
302 width: layout.bounds().width - self.outset * 2.0,
303 height: self.stroke_width,
304 },
305 border: iced_core::Border {
306 radius: iced_core::border::Radius::default()
307 .bottom_right(style.radius.top_right)
308 .bottom_left(style.radius.top_left),
309 ..Default::default()
310 },
311 shadow: iced_core::Shadow::default(),
312 ..Default::default()
313 },
314 style.color,
315 );
316 renderer.fill_quad(
318 iced_core::renderer::Quad {
319 bounds: iced_core::Rectangle {
320 x: layout.position().x + self.outset,
321 y: top_line_y,
322 width: title_layout.position().x
323 - (layout.position().x + self.outset)
324 - self.horizontal_title_padding,
325 height: self.stroke_width,
326 },
327 border: iced_core::Border {
328 radius: iced_core::border::Radius::default().top_left(style.radius.top_left),
329 ..Default::default()
330 },
331 shadow: iced_core::Shadow::default(),
332 ..Default::default()
333 },
334 style.color,
335 );
336 renderer.fill_quad(
338 iced_core::renderer::Quad {
339 bounds: iced_core::Rectangle {
340 x: title_layout.position().x
341 + title_layout.bounds().width
342 + self.horizontal_title_padding,
343 y: top_line_y,
344 width: (layout.position().x + layout.bounds().width - self.outset)
345 - (title_layout.position().x + title_layout.bounds().width)
346 - self.horizontal_title_padding,
347 height: self.stroke_width,
348 },
349 border: iced_core::Border {
350 radius: iced_core::border::Radius::default().top_right(style.radius.top_right),
351 ..Default::default()
352 },
353 shadow: iced_core::Shadow::default(),
354 ..Default::default()
355 },
356 style.color,
357 );
358 }
359
360 fn mouse_interaction(
361 &self,
362 state: &iced_core::widget::Tree,
363 layout: iced_core::Layout<'_>,
364 cursor: iced_core::mouse::Cursor,
365 viewport: &iced_core::Rectangle,
366 renderer: &Renderer,
367 ) -> iced_core::mouse::Interaction {
368 [&self.title, &self.content]
369 .iter()
370 .zip(&state.children)
371 .zip(layout.children())
372 .map(|((child, state), layout)| {
373 child
374 .as_widget()
375 .mouse_interaction(state, layout, cursor, viewport, renderer)
376 })
377 .max()
378 .unwrap_or_default()
379 }
380
381 fn update(
382 &mut self,
383 state: &mut iced_core::widget::Tree,
384 event: &iced_core::Event,
385 layout: iced_core::Layout<'_>,
386 cursor: iced_core::mouse::Cursor,
387 renderer: &Renderer,
388 clipboard: &mut dyn iced_core::Clipboard,
389 shell: &mut iced_core::Shell<'_, Message>,
390 viewport: &iced_core::Rectangle,
391 ) {
392 for ((child, state), layout) in [&mut self.title, &mut self.content]
393 .iter_mut()
394 .zip(&mut state.children)
395 .zip(layout.children())
396 {
397 child.as_widget_mut().update(
398 state, event, layout, cursor, renderer, clipboard, shell, viewport,
399 );
400 }
401 }
402
403 fn operate(
404 &mut self,
405 state: &mut iced_core::widget::Tree,
406 layout: iced_core::Layout<'_>,
407 renderer: &Renderer,
408 operation: &mut dyn iced_core::widget::Operation,
409 ) {
410 operation.container(None, layout.bounds());
411 operation.traverse(&mut |operation| {
412 [&mut self.title, &mut self.content]
413 .iter_mut()
414 .zip(&mut state.children)
415 .zip(layout.children())
416 .for_each(|((child, state), layout)| {
417 child
418 .as_widget_mut()
419 .operate(state, layout, renderer, operation);
420 });
421 });
422 }
423
424 fn overlay<'b>(
425 &'b mut self,
426 state: &'b mut iced_core::widget::Tree,
427 layout: iced_core::Layout<'b>,
428 renderer: &Renderer,
429 viewport: &Rectangle,
430 translation: iced_core::Vector,
431 ) -> Option<iced_core::overlay::Element<'b, Message, Theme, Renderer>> {
432 let children = vec![&mut self.title, &mut self.content]
433 .into_iter()
434 .zip(&mut state.children)
435 .zip(layout.children())
436 .filter_map(|((child, state), layout)| {
437 child
438 .as_widget_mut()
439 .overlay(state, layout, renderer, viewport, translation)
440 })
441 .collect::<Vec<_>>();
442
443 (!children.is_empty()).then(|| iced_core::overlay::Group::with_children(children).overlay())
444 }
445
446 fn size_hint(&self) -> iced_core::Size<Length> {
447 self.size()
448 }
449}
450
451impl<'a, Message, Theme, Renderer> From<LabeledFrame<'a, Message, Theme, Renderer>>
452 for iced_core::Element<'a, Message, Theme, Renderer>
453where
454 Message: 'a,
455 Theme: 'a + Catalog,
456 Renderer: iced_core::Renderer + 'a,
457{
458 fn from(value: LabeledFrame<'a, Message, Theme, Renderer>) -> Self {
459 iced_core::Element::new(value)
460 }
461}