use iced_core::{Length, Pixels, Rectangle};
pub struct Style {
pub color: iced_core::Background,
pub radius: iced_core::border::Radius,
}
pub trait Catalog {
type Class<'a>;
fn default<'a>() -> Self::Class<'a>;
fn style(&self, class: &Self::Class<'_>) -> Style;
}
pub type StyleFn<'a, Theme> = Box<dyn Fn(&Theme) -> Style + 'a>;
impl Catalog for iced_widget::Theme {
type Class<'a> = StyleFn<'a, Self>;
fn default<'a>() -> Self::Class<'a> {
Box::new(|theme| Style {
color: iced_core::Background::Color(theme.extended_palette().secondary.weak.color),
radius: iced_core::border::Radius::default(),
})
}
fn style(&self, class: &Self::Class<'_>) -> Style {
(class)(self)
}
}
pub struct LabeledFrame<'a, Message, Theme, Renderer>
where
Theme: Catalog,
{
title: iced_core::Element<'a, Message, Theme, Renderer>,
content: iced_core::Element<'a, Message, Theme, Renderer>,
width: Length,
height: Length,
class: Theme::Class<'a>,
inset: f32,
outset: f32,
stroke_width: f32,
horizontal_title_padding: f32,
}
impl<'a, Message, Theme, Renderer> LabeledFrame<'a, Message, Theme, Renderer>
where
Theme: Catalog,
{
pub fn new(
title: impl Into<iced_core::Element<'a, Message, Theme, Renderer>>,
content: impl Into<iced_core::Element<'a, Message, Theme, Renderer>>,
) -> Self {
Self {
title: title.into(),
content: content.into(),
width: Length::Shrink,
height: Length::Shrink,
class: Theme::default(),
inset: 5.0,
outset: 5.0,
stroke_width: 3.0,
horizontal_title_padding: 5.0,
}
}
#[must_use]
pub fn width(mut self, width: impl Into<Length>) -> Self {
self.width = width.into();
self
}
#[must_use]
pub fn height(mut self, height: impl Into<Length>) -> Self {
self.height = height.into();
self
}
#[must_use]
pub fn inset(mut self, inset: impl Into<Pixels>) -> Self {
self.inset = inset.into().0;
self
}
#[must_use]
pub fn outset(mut self, outset: impl Into<Pixels>) -> Self {
self.outset = outset.into().0;
self
}
#[must_use]
pub fn stroke_width(mut self, stroke_width: impl Into<Pixels>) -> Self {
self.stroke_width = stroke_width.into().0;
self
}
#[must_use]
pub fn horizontal_title_padding(mut self, padding: impl Into<Pixels>) -> Self {
self.horizontal_title_padding = padding.into().0;
self
}
#[must_use]
pub fn style(mut self, style: impl Fn(&Theme) -> Style + 'a) -> Self
where
Theme::Class<'a>: From<StyleFn<'a, Theme>>,
{
self.class = (Box::new(style) as StyleFn<'a, Theme>).into();
self
}
}
impl<Message, Theme, Renderer> iced_core::Widget<Message, Theme, Renderer>
for LabeledFrame<'_, Message, Theme, Renderer>
where
Renderer: iced_core::Renderer,
Theme: Catalog,
{
fn size(&self) -> iced_core::Size<Length> {
iced_core::Size::new(self.width, self.height)
}
fn layout(
&mut self,
tree: &mut iced_core::widget::Tree,
renderer: &Renderer,
limits: &iced_core::layout::Limits,
) -> iced_core::layout::Node {
let limits = (*limits).height(self.height).width(self.width).loose();
let title_layout = self
.title
.as_widget_mut()
.layout(
&mut tree.children[0],
renderer,
&limits.shrink(
iced_core::Padding::default()
.left(
(self.outset + self.inset + self.stroke_width) * 2.0
+ self.horizontal_title_padding,
)
.right(
(self.outset + self.inset + self.stroke_width) * 2.0
+ self.horizontal_title_padding,
),
),
)
.translate([
(self.outset + self.inset + self.stroke_width) * 2.0
+ self.horizontal_title_padding,
0.0,
]);
let content_layout = self
.content
.as_widget_mut()
.layout(
&mut tree.children[1],
renderer,
&limits.shrink(
iced_core::Padding::default()
.left(self.outset + self.inset + self.stroke_width)
.right(self.outset + self.inset + self.stroke_width)
.bottom(self.outset + self.inset + self.stroke_width)
.top(
(self.outset + self.inset + self.stroke_width)
.max(title_layout.size().height),
),
),
)
.translate([
self.outset + self.inset + self.stroke_width,
(self.outset + self.inset + self.stroke_width).max(title_layout.bounds().height),
]);
iced_core::layout::Node::with_children(
limits.resolve(
self.width,
self.height,
iced_core::Size::new(
(content_layout.size().width
+ (self.outset + self.inset + self.stroke_width) * 2.0)
.max(
title_layout.size().width
+ (self.outset + self.inset + self.stroke_width) * 4.0,
),
content_layout.bounds().y
+ content_layout.size().height
+ (self.outset + self.inset + self.stroke_width),
),
),
vec![title_layout, content_layout],
)
}
fn children(&self) -> Vec<iced_core::widget::Tree> {
vec![
iced_core::widget::Tree::new(&self.title),
iced_core::widget::Tree::new(&self.content),
]
}
fn diff(&self, tree: &mut iced_core::widget::Tree) {
tree.diff_children(&[&self.title, &self.content]);
}
fn draw(
&self,
tree: &iced_core::widget::Tree,
renderer: &mut Renderer,
theme: &Theme,
style: &iced_core::renderer::Style,
layout: iced_core::Layout<'_>,
cursor: iced_core::mouse::Cursor,
viewport: &iced_core::Rectangle,
) {
[&self.title, &self.content]
.iter()
.zip(&tree.children)
.zip(layout.children())
.for_each(|((child, state), layout)| {
child
.as_widget()
.draw(state, renderer, theme, style, layout, cursor, viewport);
});
let style = theme.style(&self.class);
let title_layout = layout.children().next().expect("missing title layout");
let top_line_y =
title_layout.position().y + (title_layout.bounds().height - self.stroke_width) / 2.0;
renderer.fill_quad(
iced_core::renderer::Quad {
bounds: iced_core::Rectangle {
x: layout.position().x + self.outset,
y: top_line_y,
width: self.stroke_width,
height: layout.bounds().height
- self.outset
- (top_line_y - layout.position().y),
},
border: iced_core::Border {
radius: iced_core::border::Radius::default()
.top_left(style.radius.top_left)
.bottom_left(style.radius.bottom_left),
..Default::default()
},
shadow: iced_core::Shadow::default(),
..Default::default()
},
style.color,
);
renderer.fill_quad(
iced_core::renderer::Quad {
bounds: iced_core::Rectangle {
x: layout.position().x + layout.bounds().width
- self.outset
- self.stroke_width,
y: top_line_y,
width: self.stroke_width,
height: layout.bounds().height
- self.outset
- (top_line_y - layout.position().y),
},
border: iced_core::Border {
radius: iced_core::border::Radius::default()
.top_right(style.radius.top_right)
.bottom_right(style.radius.bottom_right),
..Default::default()
},
shadow: iced_core::Shadow::default(),
..Default::default()
},
style.color,
);
renderer.fill_quad(
iced_core::renderer::Quad {
bounds: iced_core::Rectangle {
x: layout.position().x + self.outset,
y: layout.position().y + layout.bounds().height
- self.outset
- self.stroke_width,
width: layout.bounds().width - self.outset * 2.0,
height: self.stroke_width,
},
border: iced_core::Border {
radius: iced_core::border::Radius::default()
.bottom_right(style.radius.top_right)
.bottom_left(style.radius.top_left),
..Default::default()
},
shadow: iced_core::Shadow::default(),
..Default::default()
},
style.color,
);
renderer.fill_quad(
iced_core::renderer::Quad {
bounds: iced_core::Rectangle {
x: layout.position().x + self.outset,
y: top_line_y,
width: title_layout.position().x
- (layout.position().x + self.outset)
- self.horizontal_title_padding,
height: self.stroke_width,
},
border: iced_core::Border {
radius: iced_core::border::Radius::default().top_left(style.radius.top_left),
..Default::default()
},
shadow: iced_core::Shadow::default(),
..Default::default()
},
style.color,
);
renderer.fill_quad(
iced_core::renderer::Quad {
bounds: iced_core::Rectangle {
x: title_layout.position().x
+ title_layout.bounds().width
+ self.horizontal_title_padding,
y: top_line_y,
width: (layout.position().x + layout.bounds().width - self.outset)
- (title_layout.position().x + title_layout.bounds().width)
- self.horizontal_title_padding,
height: self.stroke_width,
},
border: iced_core::Border {
radius: iced_core::border::Radius::default().top_right(style.radius.top_right),
..Default::default()
},
shadow: iced_core::Shadow::default(),
..Default::default()
},
style.color,
);
}
fn mouse_interaction(
&self,
state: &iced_core::widget::Tree,
layout: iced_core::Layout<'_>,
cursor: iced_core::mouse::Cursor,
viewport: &iced_core::Rectangle,
renderer: &Renderer,
) -> iced_core::mouse::Interaction {
[&self.title, &self.content]
.iter()
.zip(&state.children)
.zip(layout.children())
.map(|((child, state), layout)| {
child
.as_widget()
.mouse_interaction(state, layout, cursor, viewport, renderer)
})
.max()
.unwrap_or_default()
}
fn update(
&mut self,
state: &mut iced_core::widget::Tree,
event: &iced_core::Event,
layout: iced_core::Layout<'_>,
cursor: iced_core::mouse::Cursor,
renderer: &Renderer,
clipboard: &mut dyn iced_core::Clipboard,
shell: &mut iced_core::Shell<'_, Message>,
viewport: &iced_core::Rectangle,
) {
for ((child, state), layout) in [&mut self.title, &mut self.content]
.iter_mut()
.zip(&mut state.children)
.zip(layout.children())
{
child.as_widget_mut().update(
state, event, layout, cursor, renderer, clipboard, shell, viewport,
);
}
}
fn operate(
&mut self,
state: &mut iced_core::widget::Tree,
layout: iced_core::Layout<'_>,
renderer: &Renderer,
operation: &mut dyn iced_core::widget::Operation,
) {
operation.container(None, layout.bounds());
operation.traverse(&mut |operation| {
[&mut self.title, &mut self.content]
.iter_mut()
.zip(&mut state.children)
.zip(layout.children())
.for_each(|((child, state), layout)| {
child
.as_widget_mut()
.operate(state, layout, renderer, operation);
});
});
}
fn overlay<'b>(
&'b mut self,
state: &'b mut iced_core::widget::Tree,
layout: iced_core::Layout<'b>,
renderer: &Renderer,
viewport: &Rectangle,
translation: iced_core::Vector,
) -> Option<iced_core::overlay::Element<'b, Message, Theme, Renderer>> {
let children = vec![&mut self.title, &mut self.content]
.into_iter()
.zip(&mut state.children)
.zip(layout.children())
.filter_map(|((child, state), layout)| {
child
.as_widget_mut()
.overlay(state, layout, renderer, viewport, translation)
})
.collect::<Vec<_>>();
(!children.is_empty()).then(|| iced_core::overlay::Group::with_children(children).overlay())
}
fn size_hint(&self) -> iced_core::Size<Length> {
self.size()
}
}
impl<'a, Message, Theme, Renderer> From<LabeledFrame<'a, Message, Theme, Renderer>>
for iced_core::Element<'a, Message, Theme, Renderer>
where
Message: 'a,
Theme: 'a + Catalog,
Renderer: iced_core::Renderer + 'a,
{
fn from(value: LabeledFrame<'a, Message, Theme, Renderer>) -> Self {
iced_core::Element::new(value)
}
}