use iced_widget::{
Container, Row, Theme, column, container, mouse_area, opaque,
space::vertical,
stack, text,
text::{Fragment, IntoFragment},
};
use crate::core::{self, Color, Element, Length, Padding, Pixels, alignment};
pub struct Dialog<
'a,
Message,
Theme = iced_widget::Theme,
Renderer = iced_widget::Renderer,
> where
Renderer: 'a + core::text::Renderer,
Theme: 'a + Catalog,
{
is_open: bool,
base: Element<'a, Message, Theme, Renderer>,
title: Option<Fragment<'a>>,
content: Element<'a, Message, Theme, Renderer>,
buttons: Vec<Element<'a, Message, Theme, Renderer>>,
on_press: Option<Box<dyn Fn() -> Message + 'a>>,
font: Option<Renderer::Font>,
width: Length,
height: Length,
max_width: Option<f32>,
max_height: Option<f32>,
horizontal_alignment: alignment::Horizontal,
vertical_alignment: alignment::Vertical,
spacing: f32,
padding_inner: Padding,
padding_outer: Padding,
button_alignment: alignment::Vertical,
class: <Theme as Catalog>::Class<'a>,
title_class: <Theme as text::Catalog>::Class<'a>,
container_class: <Theme as container::Catalog>::Class<'a>,
}
impl<'a, Message, Theme, Renderer> Dialog<'a, Message, Theme, Renderer>
where
Renderer: 'a + core::Renderer + core::text::Renderer,
Theme: 'a + Catalog,
Message: 'a + Clone,
{
pub fn new(
is_open: bool,
base: impl Into<Element<'a, Message, Theme, Renderer>>,
content: impl Into<Element<'a, Message, Theme, Renderer>>,
) -> Self {
Self::with_buttons(is_open, base, content, Vec::new())
}
pub fn with_buttons(
is_open: bool,
base: impl Into<Element<'a, Message, Theme, Renderer>>,
content: impl Into<Element<'a, Message, Theme, Renderer>>,
buttons: Vec<Element<'a, Message, Theme, Renderer>>,
) -> Self {
let content = content.into();
let size = content.as_widget().size_hint();
Self {
is_open,
base: base.into(),
title: None,
content,
buttons,
on_press: None,
font: None,
width: size.width.fluid(),
height: size.height.fluid(),
max_width: None,
max_height: None,
horizontal_alignment: alignment::Horizontal::Center,
vertical_alignment: alignment::Vertical::Center,
spacing: 8.0,
padding_inner: 24.into(),
padding_outer: Padding::ZERO,
button_alignment: alignment::Vertical::Top,
class: <Theme as Catalog>::default(),
title_class: <Theme as Catalog>::default_title(),
container_class: <Theme as Catalog>::default_container(),
}
}
pub fn title(mut self, title: impl IntoFragment<'a>) -> Self {
self.title = Some(title.into_fragment());
self
}
pub fn on_press(mut self, on_press: Message) -> Self
where
Message: Clone,
{
self.on_press = Some(Box::new(move || on_press.clone()));
self
}
pub fn on_press_with(
mut self,
on_press: impl Fn() -> Message + 'a,
) -> Self {
self.on_press = Some(Box::new(on_press));
self
}
pub fn on_press_maybe(mut self, on_press: Option<Message>) -> Self
where
Message: Clone,
{
self.on_press =
on_press.map(|message| Box::new(move || message.clone()) as _);
self
}
pub fn width(mut self, width: impl Into<Length>) -> Self {
self.width = width.into();
self
}
pub fn height(mut self, height: impl Into<Length>) -> Self {
self.height = height.into();
self
}
pub fn max_width(mut self, max_width: impl Into<Pixels>) -> Self {
self.max_width = Some(max_width.into().0);
self
}
pub fn max_height(mut self, max_height: impl Into<Pixels>) -> Self {
self.max_height = Some(max_height.into().0);
self
}
pub fn align_left(self) -> Self {
self.align_x(alignment::Horizontal::Left)
}
pub fn align_right(self) -> Self {
self.align_x(alignment::Horizontal::Right)
}
pub fn align_top(self) -> Self {
self.align_y(alignment::Vertical::Top)
}
pub fn align_bottom(self) -> Self {
self.align_y(alignment::Vertical::Bottom)
}
pub fn align_x(
mut self,
alignment: impl Into<alignment::Horizontal>,
) -> Self {
self.horizontal_alignment = alignment.into();
self
}
pub fn align_y(
mut self,
alignment: impl Into<alignment::Vertical>,
) -> Self {
self.vertical_alignment = alignment.into();
self
}
pub fn padding_inner(mut self, padding: impl Into<Padding>) -> Self {
self.padding_inner = padding.into();
self
}
pub fn padding_outer(mut self, padding: impl Into<Padding>) -> Self {
self.padding_outer = padding.into();
self
}
pub fn spacing(mut self, spacing: impl Into<Pixels>) -> Self {
self.spacing = spacing.into().0;
self
}
pub fn align_buttons(
mut self,
align: impl Into<alignment::Vertical>,
) -> Self {
self.button_alignment = align.into();
self
}
pub fn font(mut self, font: impl Into<Renderer::Font>) -> Self {
self.font = Some(font.into());
self
}
pub fn push_button(
mut self,
button: impl Into<Element<'a, Message, Theme, Renderer>>,
) -> Self {
self.buttons.push(button.into());
self
}
pub fn push_button_maybe(
self,
button: Option<impl Into<Element<'a, Message, Theme, Renderer>>>,
) -> Self {
if let Some(button) = button {
self.push_button(button)
} else {
self
}
}
pub fn extend_buttons(
self,
buttons: impl IntoIterator<Item = Element<'a, Message, Theme, Renderer>>,
) -> Self {
buttons.into_iter().fold(self, Self::push_button)
}
pub fn backdrop(self, color: impl Into<Color>) -> Self
where
<Theme as Catalog>::Class<'a>: From<StyleFn<'a, Theme>>,
{
let backdrop_color = color.into();
self.style(move |_theme| Style { backdrop_color })
}
#[must_use]
pub fn style(mut self, style: impl Fn(&Theme) -> Style + 'a) -> Self
where
<Theme as Catalog>::Class<'a>: From<StyleFn<'a, Theme>>,
{
self.class = (Box::new(style) as StyleFn<'a, Theme>).into();
self
}
#[must_use]
pub fn title_style(
mut self,
style: impl Fn(&Theme) -> text::Style + 'a,
) -> Self
where
<Theme as text::Catalog>::Class<'a>: From<text::StyleFn<'a, Theme>>,
{
self.title_class = (Box::new(style) as text::StyleFn<'a, Theme>).into();
self
}
#[must_use]
pub fn container_style(
mut self,
style: impl Fn(&Theme) -> container::Style + 'a,
) -> Self
where
<Theme as container::Catalog>::Class<'a>:
From<container::StyleFn<'a, Theme>>,
{
self.container_class =
(Box::new(style) as container::StyleFn<'a, Theme>).into();
self
}
#[must_use]
pub fn class(
mut self,
class: impl Into<<Theme as Catalog>::Class<'a>>,
) -> Self {
self.class = class.into();
self
}
#[must_use]
pub fn title_class(
mut self,
class: impl Into<<Theme as text::Catalog>::Class<'a>>,
) -> Self {
self.title_class = class.into();
self
}
#[must_use]
pub fn container_class(
mut self,
class: impl Into<<Theme as container::Catalog>::Class<'a>>,
) -> Self {
self.container_class = class.into();
self
}
fn view(self) -> Element<'a, Message, Theme, Renderer>
where
<Theme as container::Catalog>::Class<'a>:
From<container::StyleFn<'a, Theme>>,
{
let dialog = self.is_open.then(|| {
let has_title = self.title.is_some();
let has_buttons = !self.buttons.is_empty();
let contents = Container::new(column![
self.title.map(|title| {
let text = text(title)
.size(20)
.line_height(text::LineHeight::Absolute(Pixels(26.0)))
.class(self.title_class);
if let Some(font) = self.font {
text.font(font)
} else {
text
}
}),
has_title.then_some(vertical().height(12)),
self.content
])
.padding(self.padding_inner);
let contents = if has_buttons {
contents.width(Length::Fill)
} else {
contents
};
let buttons = has_buttons.then_some(
Container::new(
Row::with_children(self.buttons)
.spacing(self.spacing)
.align_y(self.button_alignment),
)
.height(80)
.padding(self.padding_inner),
);
let max_width = self.max_width.unwrap_or(
if has_buttons && !matches!(self.width, Length::Fixed(_)) {
DEFAULT_MAX_WIDTH
} else {
f32::INFINITY
},
);
let max_height = self.max_height.unwrap_or(
if has_buttons && !matches!(self.height, Length::Fixed(_)) {
DEFAULT_MAX_HEIGHT
} else {
f32::INFINITY
},
);
let content = Container::new(column![
contents,
has_buttons.then_some(vertical()),
buttons,
])
.width(if self.width == Length::Shrink && has_buttons {
Length::Fill
} else {
self.width
})
.height(if self.height == Length::Shrink && has_buttons {
Length::Fill
} else {
self.height
})
.max_width(max_width)
.max_height(max_height)
.class(self.container_class)
.clip(true);
let backdrop = mouse_area(
container(opaque(content))
.style(move |theme| container::Style {
background: Some(
Catalog::style(theme, &self.class)
.backdrop_color
.into(),
),
..Default::default()
})
.padding(self.padding_outer)
.width(Length::Fill)
.height(Length::Fill)
.align_x(self.horizontal_alignment)
.align_y(self.vertical_alignment),
);
if let Some(on_press) = self.on_press {
opaque(backdrop.on_press(on_press()))
} else {
opaque(backdrop)
}
});
stack![self.base, dialog].into()
}
}
pub const DEFAULT_MAX_WIDTH: f32 = 400.0;
pub const DEFAULT_MAX_HEIGHT: f32 = 260.0;
impl<'a, Message, Theme, Renderer> From<Dialog<'a, Message, Theme, Renderer>>
for Element<'a, Message, Theme, Renderer>
where
Renderer: 'a + core::Renderer + core::text::Renderer,
Theme: 'a + Catalog,
Message: 'a + Clone,
<Theme as container::Catalog>::Class<'a>:
From<container::StyleFn<'a, Theme>>,
{
fn from(dialog: Dialog<'a, Message, Theme, Renderer>) -> Self {
dialog.view()
}
}
#[derive(Debug, Clone, Copy, PartialEq)]
pub struct Style {
pub backdrop_color: Color,
}
pub trait Catalog: text::Catalog + container::Catalog {
type Class<'a>;
fn default<'a>() -> <Self as Catalog>::Class<'a>;
fn default_title<'a>() -> <Self as text::Catalog>::Class<'a> {
<Self as text::Catalog>::default()
}
fn default_container<'a>() -> <Self as container::Catalog>::Class<'a> {
<Self as container::Catalog>::default()
}
fn style(&self, class: &<Self as Catalog>::Class<'_>) -> Style;
}
pub type StyleFn<'a, Theme> = Box<dyn Fn(&Theme) -> Style + 'a>;
impl Catalog for Theme {
type Class<'a> = StyleFn<'a, Self>;
fn default<'a>() -> <Self as Catalog>::Class<'a> {
Box::new(default)
}
fn default_container<'a>() -> <Self as container::Catalog>::Class<'a> {
Box::new(|theme| container::background(theme.palette().background))
}
fn style(&self, class: &<Self as Catalog>::Class<'_>) -> Style {
class(self)
}
}
pub fn default<Theme>(_theme: &Theme) -> Style {
Style {
backdrop_color: core::color!(0x000000, 0.3),
}
}